Arduino

2025年3月22日 (土)

Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMの例

analogReadでの電圧値変換問題(1023 vs 1024)だけじゃなく
2020年8月13日:Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMを使おう
analogWriteにも噛み付いています。

で、まっとうなPWM出力制御の例を出していなかったかと
  (アプリケーション・スケッチの中では
   いっぱい使っているんで探せば出てくるけど)
いうことで、OC2AとOC2Bの2chをPWM出力にして
まっとうなPWMの方法(analogWriteと大きい声で
言っても恥ずかしくないような)を示しておきます。

/*****  タイマー2でまっとうなPWMを *****/
// タイマー2 PWM出力 0~255,256を許容
// 0:duty=0, 128:duty=128/256, 255:duty=255/256
// 256:duty 100%(Hレベル)
/***** PWM出力 OC2A *****/
void pwm2a(short d)
{
if(d < 0) d = 0; // 0未満は0に
if(d < 256){ // 255まで
OCR2A = (byte)(255 - d); // duty 0~255/256 PWM
TCCR2A |= 0b11000011;
// |||| ++--- 8bit 高速PWM動作
// ||++------- OC2B (PWM反転動作)
// ++--------- OC2A PWM反転動作
}
else{ // 256以上はHレベル出力
PORTB |= (1 << PB3); // PB3をHに
TCCR2A &= 0b00110011;
// |||| ++--- 8bit 高速PWM動作
// ||++------- OC2B (PWM反転動作)
// ++--------- OC2A OC2A切断
}
}

/***** PWM出力 OC2B *****/
void pwm2b(short d)
{
if(d < 0) d = 0; // 0未満は0に
if(d < 256){ // 255まで
OCR2B = (byte)(255 - d); // duty 0~255/256 PWM
TCCR2A |= 0b00110011;
// |||| ++--- 8bit 高速PWM動作
// ||++------- OCR2B PWM反転動作
// ++--------- OCR2A (PWM反転動作)
}
else{ // 256以上はHレベル出力
PORTD |= (1 << PD3); // PD3をHに
TCCR2A &= 0b11000011;
// |||| ++--- 8bit 高速PWM動作
// ||++------- OCR2B OC2B切断
// ++--------- OCR2A (PWM反転動作)
}
}

/***** SETUP *****/
void setup() {
// タイマー2 PWM出力に初期化
// OC2A:PB3 , OC2B:PD3出力ポートに
DDRB |= (1 << PB3); // OC2A : PB3
DDRD |= (1 << PD3); // OC2B : PD3
// タイマー2 980Hz PWM出力
TCCR2A = 0b11110011;
// |||| ++--- 8bit 高速PWM動作
// ||++------- OC2B PWM反転動作
// ++--------- OC2A PWM反転動作
TCCR2B = 0b00000100;
// || |+++--- CS 1/64 250kHz → 1/256 976.6Hz(1.024ms)
// || +------ WGM22
// ++--------- FOC2
OCR2A = 255 - 0; // duty 0に
OCR2B = 255 - 0; // duty 0
TIMSK2 = 0b00000000;
// ||+--- TOIE2 割込オフ
// |+---- OCIE2A
// +----- OCIE2B
// タイマー0,1と2の前置分周器をリセット
GTCCR = 0b10000000;
// | |+--- PSRSYNC タイマ0,1
// | +---- PSRASY タイマ2
// +---------- TSM タイマ同期動作
GTCCR = 0b10000011; // リセット操作
TCNT0 = 0;
TCNT2 = 0;
GTCCR = 0b00000000; // 解除
}

// 出力データ
struct st_pwm{
short p0a; // OC0A PD6 (6)
short p0b; // OC0B PD5 (5)
short p2a; // OC2A PB3 (11)
short p2b; // OC2B PD3 (3)
};
// 順に出力
st_pwm pwm_tbl[]={
{ 1, 0, 0, 254 }, // <0>
{ 1, 1, 1, 255 }, // <1>
{ 1, 2, 2, 256 }, // <2>
{ 1, 254, 254, 0 }, // <3>
{ 1, 255, 255, 1 }, // <4>
{ 1, 256, 256, 2 }, // <5>
}; // | | | +--- OC2B PD3 (3)
// | | +-------- OC2A PB3 (11)
// | +------------- OC0B PD5 (5)
// +------------------- OC0A PD6 (6)

/***** LOOP *****/
void loop() {
byte i;
while(1){
for(i = 0; i < 6; i++){
analogWrite(6, pwm_tbl[i].p0a); // OC0A (6)
analogWrite(5, pwm_tbl[i].p0b); // OC0B (5)
pwm2a(pwm_tbl[i].p2a); // OC2A (11)
pwm2b(pwm_tbl[i].p2b); // OC2B (3)
delay(2000); // 2秒待ち
}
}
}
// 2025-03-22 JH3DBO

元のanalogWrite、これの何がおかしいのか気付いたのが
  ・2020年2月10日:ArduinoのanalogWrite 1/255なの?
このあたり。

~~~~~~~~~~~~~~~~~~~~~~~~~~~
タイマー0のanalogWriteに対して、0~255の数値を順に
与えてみると・・・周期1.024msのパルスに対して、
  0ならずっとLOW
  1だと8usのHIGHパルス  ▼1
  2だと12usのHIGHパルス
   :
  253だと8usのLOWパルス
  254だと4usのLOWパルス
  255だとずっとHIGH
が観察されます。
タイマー0に関して、値0と1のところ▼1で、ほんとなら4usステップ
になって欲しいのが8usとなっていて、値1~255とは異なるピッチ
なっていることがわかります。

デューティ50%が欲しい時は127。
普通の8bit D/Aコンバータだと128で半値がでてきます。

8bit D/Aコンバータの代わりになるようなPWMだと、どんな信号に
なるかというと。
  0 → デューティ0%で 全区間L
  1 → デューティ 1/256%
  2 → デューティ 2/256%
  :
 128 → デューティ 128/256% = 50% 方形波
  :
 254 → デューティ 254/256%
 255 → デューティ 255/256%
  ここでおしまい。 全区間Hはなし
~~~~~~~~~~~~~~~~~~~~~~~~~~

「半値」や「1/4値」でチェックすると、「何かおかしい」
と気付くはずなんですが・・・

上に上げた「まっとうなPWM」では、0~255で
0/256~255/256を出力。
そして8bitを越えるけど256でduty 100%となるような
処理を入れ込みました。

オシロで波形を観察するとこんな様子になります。
OC0Aがトリガー用。
OC0BとOC2Aに同じ値を入れてます。
I01b

2020年8月16日:Arduino UNOのPWM出力を(ちょっと精密に)確かめる

analogWriteも
2022年11月12日:『1/1023 vs 1/1024』問題 analogReadを2bitにして確かめてみる
と同じように2bitにしたら分かるかな。
  0はゼロ。
  1で1/4の1.25V。
  2で半値の2.50V。
  3で3.75Vが出てきます。
5.00Vは2bitのD/Aコンバータでは出せません。

  Vout = (Vref * data) / (2^n)
という式で出力が出てきます。

フルスケールの3で5.00Vを出したいとなると、
  0はゼロ。
  1で1.67V。
  2で3.33V。
  3で5.00V。
アナログ的なゲイン調整でこういったことはできますが、
半値がどこかへ行ってしまいます。

| | コメント (0)

2025年1月28日 (火)

パルスジェネレータをI2C液晶で動かす

トラ技2025年2月号のトラ技Jr.コーナに掲載してもらった
  0.1 ~9999.99Hzの2相パルス発生回路の製作
  Arduino Uno R3を使って1/nカウント方式も
  DDS方式も動かす!

Tr02a

この記事に掲載されたハードウェアで
 ・2022年8月24日:パルスジェネレータを作ってみた:箱に入れた
これが動くようにしました。
「箱に入れた」からの変更点は、
 ・パラレル接続の液晶をI2Cタイプに
 ・PWM出力duty sweepモードを追加
   SW4長押しで出力極性+/-を反転
   させていたのをパラメータ設定モードに。
   sweep開始と終了duty比と増加減時間を
   パラメータとして設定。
   出力極性の反転もこの中で。
 ・Low Bat警報は液晶の右下隅に電池マークを表示。
です。

スケッチ本体(inoファイル)とI2C液晶ルーチンを
圧縮しています。
  ・ダウンロード - pgen_i2lcd1.zip



トラ技の記事、最初の1ページだけ ですがpdfになっ
てます。

参考図
・トラ技に掲載された2相パルス発生回路の
 接続図
  Arduino-UNO-R3でのつなぎ

Z12_20241013173501
  I2C液晶で表示

・「箱」に入れた回路図
  同じくArduino-UNO-R3でのつなぎ
Pa11  
パラレル接続液晶を4bitモードで使ってる。

2024年10月13日:1/nカウント方式とDDS方式の2相パルス発生回路

※バックライト無しのI2C液晶での表示

出力パルス極性の「POS NEG」表示をグラフィック的に変更。
P51_20250129104801
P52_20250129104901

Low Batt警報を液晶の右下に。
電圧2,0V以下に低下で、電池カラっけつと
もう半分だぜを交互に表示。
P53
P54
電池電圧正常時は表示無し。

重要アラームは点滅させて欲しい への対応ということで。

Duty Sweep中の表示
P55
スイープ中でも「周期」の変更は可能。

| | コメント (0)

2024年12月26日 (木)

EEPROMを使ったシリアル受信バッファ 512kバイトに増設

 備忘録として。

まだ、まとめの途中ですが「シリアルデータ記録装置」の
メモリーを512kバイトに拡張する作業を進めてます。
  ※回路図とスケッチはちょい待って。

Rr11_20241225180101
Rr12_20241225180101
Rr13

2020年3月16日:シリアルデータ記録装置を
2020年3月17日:ICの2段重ねで容量アップ
2020年3月20日:256kBシリアルデータ記録回路とりあえず完成

もともとは128kバイトのEEPROM、24LC1025
これを2段重ねにして256kバイトにして使っていたの
ですが、さらに重ねて「四重」に。
  (二重×2)
そうしたら512kバイトに。

記録するデータはゆっくりと9600BPSで受け
ているのですが、取り込んだデータを吐き出す
時は「早いほうが良いぞ」ということで、
送出を115.2kBPSにスピードアップしたら
どうだという改造(ソフト的な)をしてみたのです。

SDカードへを使ってのシリアルデータの取り込み
だと、データの取り出し速度は取り込み装置とは
別のものとして切り離せます。
しかし、今回のツールでは、自分が持つメモリー
へ書き込んだデータを取り出すには、シリアル通信しか
出口がありません。
ということで、「早よせんかい!」の罵声には通信速度の
アップしか方法がないのです。
で、あれこれ試している途中なのです。

※あれこれ
・書き込みはすでにページ書き込みを実現して
 いるので、取りこぼしはない。
   ターゲット装置との兼ね合いで、受け速度
   は9600BPSのままゆっくりで。
・取り込んだデータの吐き出しを115.2kBPSに
 スピードアップしようとすると、EEPROMの読み
 出し速度が問題に。
 1バイト単位での読み出しだと遅いのだ。
・スピードアップにはページ読み出しでバッファリング
 しながら送出ということになるのだが、Arduinoの
 ライブラリの制限でまとまりは30バイトが最大。
   (書き込みも同じ)
・それでも、1バイト単位での読み出しより早い。

EEPROMのページ書き込み、ページ読み出し手順の
の参考になるかと。

まず、回路図。
Serial_log3
2段重ねの24LC1025(24FC1025)を2ブロック。
これで512kバイトで、24LC1025が使えるアドレス範囲
0x00000~07FFFFを使い切りました。

その制御スケッチ。(元がArduino UNO R3)ですんで)
    ダウンロード - rxbff3a.txt

ファイル名を".ino"から".txt"に変えていますんで、
そのままエディタで読めるはず。

※改良点
・JP1をonすると、取り込んだシリアルデータを出力する
 ときの通信速度を115.2kBPSにして、12倍にスピードアップ。
   データ取り込み時の速度は9600BPSのまま。

・EEPROMのアクセス、ライブラリ(twi wire)を使わず、
 オリジナルのルーチンに。
   割り込みでの処理がないのですっきり。

・EEPROMをページアクセスするためのバッファも
 その大きさを自由にできちゃう。
   元のは30バイトまでという制限があった。

※参考にしてもらいたいところ

・EEPROMの書き込み、多くのサンプルプログラムが
   書いて → 待つ(時間で5msあるいは「ACKポーリング」で)
 をしています。
   これだと、書いた後は、何もできないのでもったいない。
 時間待ちは変わりませんが、
   待って → 書く、あるいは読む
 にしています。
 書き込みの完了でタイマーをセット。
   割り込みでダウンカウント。
 次に読み書きする時はそのタイムアップをチェックします。
 書き込み後、すぐ別の処理ができますんで、時間待ちの
 無駄が無くせます。
   今回のだとシリアルデータの受信処理を
   この5ms間も継続実行してます。

・ページをまたいだEEPROMの読み書きも、見てもらい
 ところです。
 多くのサンプルプログラムが、ページの境界を知らん
 ふりしています。
 24LC1025(24FC1025)の場合、ページは128バイト。
 連続していれば、1アクションで書き込み出来ます。
 しかし、書き込みバイト数に対して、アドレスが128バイトの
 境界で分断してしまうと、先と後の2回に分けて書き込み
 しなくてはなりません。
 例えば・・・
   先頭アドレス0x01234から100バイト(0x64)書きたい。
   アドレス0x01280がページの境界。
   0x01234~0x0127F と0x01280~0x1297の2回に
   分けて処理しなければならない。

   また、読み出しでは64kバイトごとの境界も関係して
   きます。
   ページ数の128バイトに関係なく連続して読み出しで
   きますが、16bitのアドレスを越えて読み出しすると、
   同じブロックの先頭に戻ってしまいうので、書き込み
   と同じように分割しての処理を考えなければなりません。

 お手軽に大容量メモリーが使えるのですが、
   「こんな処理で応用できるんかいな」
 っというサンプルが目に付いちゃいます。

・ライブラリ、wireとtwiを使わないようにしたら
 すいぶんと使用メモリーが減りました。
  ROMが7094バイトが4710バイトになって2384バイトの減
  RAMが1032バイトが888バイトになって144バイトの減
    (読み書きバッファを68バイト増やしてるのに)

※関連
2021年11月26日:EEPROM、 2kバイトと4kバイトの間には壁がある (24LC16で)
2024年4月12日:I2C液晶のアクセス、割り込みで処理しないようにすると
2021年7月2日:秋月の液晶表示器 ACM0802C-NLW-BBW-IIC、I2Cのプルアップ抵抗
2021年11月27日:EEPROM I2Cのクロックを早くすると・・・



| | コメント (0)

2024年10月13日 (日)

1/nカウント方式とDDS方式の2相パルス発生回路

Arduino UNO R3を使った2相パルス発生回路
 ・1/nカウント方式:タイマ1の分周器を制御
 ・DDS方式:クロック80kHzでDDS処理
のスケッチ、バックアップがわりに置いておきます。
   ・ダウンロード - cw_ccw.zip

2つの回路図は同じ。
Arduino UNO R3で試すならこちらを。
Z12_20241013173501

ATmega328Pで手組みするならこんな感じ。

Z13_20241013173601

※関連
2024年10月5日:DDS方式の2相パルス発生回路、周波数スキャン機能を付ける
2024年9月24日:秋月のI2C接続液晶 AQM1602XAを基板に直付け

※1/nカウント方式2相パルス発生回路の概略

・出力周波数範囲  0.1Hz~9990.0Hz
  ロータリ・エンコーダで周波数を設定
  周波数により設定できる最小桁が変化
    0.1~299.9Hz : 0.1Hz単位で設定
    300~ 2999Hz : 1Hz単位で設定
   3000~ 9990Hz : 10Hz単位で設定
・指定した数のパルスを出力する機能を持つ
  1~999999の範囲で指定した数のパルスを出力して停止
・タイマ1のクロック入力周波数÷目的周波数で分周比(16bit整数)を
 計算しているので、周波数が高くなると誤差が出る
・設定周波数に対する実出力周波数の誤差を液晶表示
・1/nで出力周波数を得ているのでジッタは発生しない
・電池電圧低下で警報表示
・スイッチ機能(短押し)
 SW1 設定した数のパルスをCCWで出力
 SW2 パルス出力を停止
 SW3 設定した数のパルスをCWで出力
 SW4 周波数とパルス数の設定入力の切り替え
・スイッチ機能(長押し)
 SW1 CCWでパルスを連続出力
 SW2 設定値をEEPROMに保存
 SW3 CWでパルスを連続出力
 SW4 パルス・カウント・エッジ↓↑の切り替え


※DDS方式2相パルス発生回路の概略

・出力周波数設定範囲  0.10Hz~9999.99Hz
  0.01Hz単位
  入力位置はカーソル移動で
  ロータリ・エンコーダで設定
・DDSの基準クロックは80kHz
・ジッタが発生するが平均すると設定周波数が得られる
・電池電圧低下で警報表示
・画面を切り替えて周波数スイープ機能のパラメータを設定
  周波数の下限,上限
  rise,fall時間
  上限,下限でのwait時間
・スイッチ機能(短押し)
 SW1 パルス出力on/off
 SW2 周波数入力カーソル位置を左へ
 SW3 周波数入力カーソル位置を右へ
 SW4 sweep開始
・スイッチ機能(長押し)
 SW1 設定値をEEPROMに保存
 SW2 CCWでパルスを連続出力
 SW3 CWでパルスを連続出力
 SW4 sweep機能のパラメータ設定


出力パルスを「周波数」で設定できます。
1/nカウントは「整数分の1」ですんで誤差が生じます。
  (液晶に設定値と出力値の誤差を表示)
DDS方式のは0.01Hzを可変できるようにしたんで
ちょい面白いかも。 ただし、ジッタが出ます。

※追記
1/nパルス方式もDDS方式も2相パルスを出しますが、
その1相だけ(単発パルスとして)を使っても便利です。

・1/nパルス方式なら「60Hzのパルスを120発出して停止」。
何て機能、これを備えたツール、なかなかありませんよ。
パルス数の最大が99999発と16bit越え。

・DDS方式のスイープが便利。
59Hz~61Hzを10分かけてup/downという設定も可能。
最小桁が0.01Hzですんで、59.01、59.02、59.03・・・
と周波数が変化します。
一定周期で周波数が振る信号、たまに欲しい時があります。
  ・・・このツールでは方形波ですが


| | コメント (0)

2024年10月 6日 (日)

おっと。map関数の計算桁に注意

Arduinoのmap関数に関して恨みを持っている
がごとくウダウダ言っております。
 ・2020年5月17日:Arduino なんとかして誤用を正したい:A/Dの1/1023とmap関数

トラ技にも載りました。
 ・トランジスタ技術2024年3月号『実はワナだらけ…確実に動かすArduino Uno R3

で、今回の
 ・2024年10月5日:DDS方式の2相パルス発生回路、周波数スキャン機能を付ける

ここで、「おっと危ない」だったのが「map」関数の数値範囲。

周波数sweepでは、
  周波数の値は0.01Hz単位で9999.99Hzが最大。
  BCDで8桁。999999=0xF423F 20bit

  経過時間が10ms単位で600.00秒が最大。
  BCDで5桁。60000=0xEA60 16bit

sweep処理では、こんな値を使います。
  Lo側周波数 1~9999Hz   f1
  Hi側周波数 1~9999Hz   f2
  Rise,fall時間 0.0~600.0秒 t1

sweep周波数の設定は1Hz単位ですが、内部では0.01Hz
単位に直します。
sweep時間も0.1秒を0.01秒にして60000が最大。

そして、「map関数」はこのように定義されています。

long map(long x,         入力
  long in_min, long in_max,  X1,X2 変換元
  long out_min, long out_max) Y1,Y2 変換先
{
 return (x - in_min) * (out_max - out_min) ★1
   /  (in_max - in_min)
   + out_min;
}

sweep処理で経過時間tmからsweep周波数fmを計算するとき、
map関数をこのように使いました。

  fm = map (tm , 0, t1, f1, f2);

このとき、周波数と経過時間が設定の最大値に近づくと、
★1の乗算に失敗するのです。

周波数(20bit)と時間(16bit)を掛け算すると、
値によっては「36bit」の数字が出てきます。

map関数の入力と出力、単独で見るとそれぞれは
32bitの数値範囲に入っていますが、内部計算を考えると
32bitではアウトなのです。

内部は「int64_t」で計算しなくちゃなりません。

 return ((int64_t)(x - in_min) *
     (int64_t)(out_max - out_min))
     / ((int64_t)(in_max - in_min))
     + out_min;

乗算部、除算部をint64_tにキャストして64bitで
計算するようにしました。

LOG sweepではfloatにしたmapも使っています。

float mapf(float x,
  float in_min, float in_max,
  float out_min, float out_max)

2022年11月6日:Arduino map関数をfloatに

もうひとつ。
t1がゼロだとゼロ除算してしまうので、mapに
食わす前に判定して除外しておかなくちゃなりません。
t1がゼロなら fm=f1でリターンてな処理です。

| | コメント (0)

2024年10月 5日 (土)

DDS方式の2相パルス発生回路、周波数スキャン機能を付ける

あれこれ悩みながらこしらえてきたDDS方式の2パルス発生回路
ざっと完成形に持ち込めました。
割り込み処理の時間はギリギリで動いていますが、
メインループはヒマ
ということで、周波数スキャン(sweep)機能を付加しました。
設定するのは、
  Lo側周波数 1~9999Hz   f1
  Hi側周波数 1~9999Hz   f2
  Rise時間  0.0~600.0秒  t1
  Fall時間  0.0~600.0秒  t2
  Hi待ち時間 0.0~600.0秒  w1
  Lo待ち時間 0.0~600.0秒  w2

スイープは
 (1) f1周波数で2相パルス出力
 (2) t1時間をかけてf2までパルス周波数を上昇
 (3) f2周波数をw1時間保持
 (4) f2周波数からf1まで周波数を下降
 (5) f1周波数をw2時間保持
 (6) (1)に戻る
を繰り返します。

「100Hz~400Hz」をスイープすると、こんな波形が出てきます。

Bb002_20241005115101
   変化点を拡大
Bb000_20241005115101

一番下の波形は「F-Vコンバータ」 で見た、
A相パルスの周波数変化。

まだ「main loopはヒマ」だからと、LOG SWEEPの処理も
書き加えてみました。
A相周波数がこんな具合に変化します。
Bb001_20241005115401

Arduino UNO R3のATmega328P(16MHzクロック)、
8bitマイコンなのに、意外と浮動小数点処理を頑張って
くれます。
10msごとに経過時間から周波数を計算しています。

※関連
2024年9月24日:秋月のI2C接続液晶 AQM1602XAを基板に直付け
2024年9月27日:1クロックでも速くしたい DDS方式の2相パルス発生器
2024年9月30日:1クロックでも速くしたい 割込を「ISR_NAKED」で

※回路図
Cw_ccw2

元のでは無しにしていたSW4にsweepの操作を割り当てています。


| | コメント (0)

2024年9月30日 (月)

1クロックでも速くしたい 割込を「ISR_NAKED」で

2024年9月27日:1クロックでも速くしたい DDS方式の2パルス発生器

この続き。 タイマー1のコンペアマッチA割り込みを
インライン・アセンブラで書いてみました。

/***************************/
/* DDSでパルス出力 */
/***************************/
// A相B相出力データ COM1A,COM1Bで出力
volatile uint8_t com1_ab; // 3bitでCW,CCW判断
// tbl_ab[]をアクセス
volatile uint8_t tbl_ab[]={
0b11110000, // 000 H H CW
0b10110000, // 001 L H
0b10100000, // 010 L L
0b11100000, // 011 H L
0b11100000, // 100 H L CCW
0b10100000, // 101 L L
0b10110000, // 110 L H
0b11110000, // 111 H H
}; // |||| ++---- CTC OCR1A モード
// ||++-------- COM1B PB2 (00:off,01トグル,10:L,11:H)
// ++---------- COM1A PB1

/***** タイマー1 : コンペアマッチA割り込み *****/
// DDS処理
// CW/CCW出力 A:PB1(OC1A) B:PB2(OC1B)
// COM1(A,B) 00:off,01トグル,10:L,11:H
// PB4_H; // (!!!)
// dds_cnt += dds_step; // DDSカウンタをカウントアップ
// if(pls_on){ // パルス出力中
// com1_ab = ([dds_cnt.cnt1+3] / 4) & 0x03; // 1/(65536*4096)
// com1_ab |= pls_dir; // bit2でCW,CCWを区分
// }
// TCCR1A = tbl_ab[com1_ab]; // COM1A,COM1B制御 offなら前のb値で
// PB4_L; // (!!!)
ISR(TIMER1_COMPA_vect, ISR_NAKED)
{
__asm__ volatile(
" sbi %0, 4 \n" // PB4 H
" push r0 \n"
" in r0, __SREG__ \n"
" push r28 \n"
" push r29 \n"
" push r30 \n"
" push r31 \n"
// dds_cnt += dds_step;
" lds r28, (dds_step) \n" // LSB
" lds r30, (dds_cnt) \n"
" add r30, r28 \n"
" sts (dds_cnt), r30 \n"
" lds r28, (dds_step+1) \n" // +1
" lds r30, (dds_cnt+1) \n"
" adc r30, r28 \n"
" sts (dds_cnt+1), r30 \n"
" lds r28, (dds_step+2) \n" // +2
" lds r30, (dds_cnt+2) \n"
" adc r30, r28 \n"
" sts (dds_cnt+2), r30 \n"
" lds r28, (dds_step+3) \n" // +3
" lds r30, (dds_cnt+3) \n"
" adc r30, r28 \n"
" sts (dds_cnt+3), r30 \n"
// if(pls_on){
" lds r28, (pls_on) \n"
" and r28, r28 \n"
" breq j1 \n"
// com1_ab = ([dds_cnt+3] / 4) & 0x03;
" lsr r30 \n" // 1/2
" lsr r30 \n" // 1/4
" andi r30, 0x03 \n" // & 0x03
// com1_ab |= pls_dir; // bit2でCW,CCWを区分
" lds r28, (pls_dir) \n"
" or r30, r28 \n"
" sts (com1_ab), r30 \n"
" j1: \n"
// TCCR1A = tbl_ab[com1_ab];
" lds r30, (com1_ab) \n" // 0~7
" ldi r31, 0 \n"
" ldi r28, lo8(tbl_ab) \n" // tbl_ab[]
" ldi r29, hi8(tbl_ab) \n"
" add r30, r28 \n"
" adc r31, r29 \n"
" ld r28, z \n"
" sts %1, r28 \n" // TCCR1A
// pop reg
" pop r31 \n"
" pop r30 \n"
" pop r29 \n"
" pop r28 \n"
" out __SREG__, r0 \n"
" pop r0 \n"
" cbi %0, 4 \n" // PB4 L
" reti \n"
:
: "n" (_SFR_IO_ADDR(PORTB)), // %0
"n" (_SFR_MEM_ADDR(TCCR1A)) // %1
:
);
}

「ISR_NAKED」で、使うレジスタを我が手に。
その結果、
  ・pushとpopするレジスタの数を吟味して減らせる
  ・タイミングを見るためのポート出力を先頭と最後に
   持ってこれるので、正確に見える。
問題はインライン・アセンブラの書式。
AVRマイコン特有の問題がまだ解決できていません。

「Z」レジスタにA相B相出力テーブルの先頭を持ってきているところ、
「c」だと「マイナスの値を引いて足し算」する処理が行われています。

   820:	e0 91 62 01   lds    r30, 0x0162
824: f0 e0 ldi r31, 0x00
826: ed 5f subi r30, 0xFD
828: fe 4f sbci r31, 0xFE
82a: 80 81 ld r24, Z  テーブル値を読み出し

これ、AVRマイコンの命令体系で、即値加算の命令が無いのです。
  即値加算:addiとかadci
そこで、即値減算命令 subi、sbci命令を使い、
  マイナスの値を引いて足し算に
ということで対処するのです。
  AVRマイコン・アセンブラの定石

ところが、インライン・アセンブラの書式で、テーブルの先頭アドレスを
マイナスにする方法が分からないのです。
  (- tbl_ab)とか(0 - tbl_ab)とかあれこれ
  試しましたが、エラーが出ちゃいます。
そこで仕方なく、レジスタ同士の加算命令を使って、アドレスを
得ています。
  これで、2命令を損

 " lds r30, (com1_ab) \n" // 0~7
 " ldi r31, 0 \n"
 " ldi r28, lo8(tbl_ab) \n" // tbl_ab[]
 " ldi r29, hi8(tbl_ab) \n"
 " add r30, r28 \n"
 " adc r31, r29 \n"
 " ld r28, z \n"  テーブル値の読み出し

これ、なんとかできれば良いのですが。

push、popするレジスタを5つまで減らせました。
   元のは13個。
これでダイブとスピードアップ。

「union」を使った32bit値と8bit値のまぜこぜ処理も
無くなったし、ある意味、すっきりと。

※実行時間 割り込み処理およそ5μ秒に。
Scope_1126_memo1
  前は8μ秒ほど

| | コメント (0)

2024年9月27日 (金)

1クロックでも速くしたい DDS方式の2相パルス発生器

DDS方式の2相パルス発生回路、現在は80kHz(12.5μs)の
タイマー割り込み内で処理しています。
こんな感じ。

/******************************/
/* DDS制御 */
/******************************/
// DDS制御データ
// dsiz(1024,2048,4096)の上位2bitでA,B相出力を決定
// タイマー1割り込みで処理 80kHz=12.5us(=TM1_FRQ)
int32_t dds_frq; // 0.01Hz単位の周波数設定値 ★1
// 0.10Hz~9999.99Hz
volatile uint32_t dds_step; // frqから計算したdds制御1step ★2
// frq * dsiz * 65536 / TM1_FRQ
// DDS設定カウンタ (uint32_t dds_cnt)
// タイマー割り込みでdds_stepを加算 上位ビットで波形出力
union u_dc{
volatile uint32_t cnt4; // 4byte DDS設定カウンタ ★3
volatile uint8_t cnt1[4]; // 1byt DDS設定カウンタ ★4
};
u_dc udds_cnt; // カウントデータ共用体
// cnt4に加算 cnt1で読み出し
// --- 3--- --- 2--- --- 1--- --- 0--- 4バイト
// 10987654 32109876 54321098 76543210
// xxxxHHbb bbbbbbbb aaaaaaaa aaaaaaaa
// || 下位26bitのところにstepを加算
// ++--- CW,CCW判定bit これを読み出したい
// xxxx----- 上位は無視してok

/***************************/
/* DDSでパルス出力 */
/***************************/
/***** タイマー1 : コンペアA割り込み *****/
// DDS処理
// CW/CCW出力 A:PB1(OC1A) B:PB2(OC1B)
// COM1(A,B) 00:off,01トグル,10:L,11:H
ISR(TIMER1_COMPA_vect)
{
static uint8_t b; // 3bitでCW,CCW判断
static uint8_t com1ab[]={
0b11110000, // 000 H H CW
0b10110000, // 001 L H
0b10100000, // 010 L L
0b11100000, // 011 H L
0b11100000, // 100 H L CCW
0b10100000, // 101 L L
0b10110000, // 110 L H
0b11110000, // 111 H H
}; // |||| ++---- CTC OCR1A モード
// ||++-------- COM1B PB2 (00:off,01トグル,10:L,11:H)
// ++---------- COM1A PB1
PB4_H; // (!!!)
udds_cnt.cnt4 += dds_step; // DDSカウンタをカウントアップ ★3+★2
if(pls_on){ // パルス出力中
b = (udds_cnt.cnt1[3] >> 2) & 0x03; // 1/(65536*4096)
b |= pls_dir; // bit2でCW,CCWを区分
}
TCCR1A = com1ab[b]; // COM1A,COM1B制御 offなら前のb値で
PB4_L; // (!!!)
}


アセンブル結果がこれ。

000007a6 <__vector_11>:
7a6: 1f 92 push r1
7a8: 0f 92 push r0
7aa: 0f b6 in r0, 0x3f ; 63
7ac: 0f 92 push r0
7ae: 11 24 eor r1, r1
7b0: 4f 93 push r20
7b2: 5f 93 push r21
7b4: 6f 93 push r22
7b6: 7f 93 push r23
7b8: 8f 93 push r24
7ba: 9f 93 push r25
7bc: af 93 push r26
7be: bf 93 push r27
7c0: ef 93 push r30
7c2: ff 93 push r31
7c4: 2c 9a sbi 0x05, 4 ; 5
7c6: 40 91 74 01 lds r20, 0x0174
7ca: 50 91 75 01 lds r21, 0x0175
7ce: 60 91 76 01 lds r22, 0x0176
7d2: 70 91 77 01 lds r23, 0x0177
7d6: 80 91 63 01 lds r24, 0x0163
7da: 90 91 64 01 lds r25, 0x0164
7de: a0 91 65 01 lds r26, 0x0165
7e2: b0 91 66 01 lds r27, 0x0166
7e6: 84 0f add r24, r20
7e8: 95 1f adc r25, r21
7ea: a6 1f adc r26, r22
7ec: b7 1f adc r27, r23
7ee: 80 93 63 01 sts 0x0163, r24
7f2: 90 93 64 01 sts 0x0164, r25
7f6: a0 93 65 01 sts 0x0165, r26
7fa: b0 93 66 01 sts 0x0166, r27
7fe: 80 91 7e 01 lds r24, 0x017E
802: 88 23 and r24, r24
804: 69 f0 breq .+26 ; 0x820 <__vector_11+0x7a>
806: 80 91 66 01 lds r24, 0x0166   ★A
80a: 90 e0 ldi r25, 0x00 ;★B
80c: 95 95 asr r25  ★B
80e: 87 95 ror r24  ★A
810: 95 95 asr r25  ★B
812: 87 95 ror r24  ★A
814: 83 70 andi r24, 0x03 ; 3
816: 90 91 7c 01 lds r25, 0x017C
81a: 89 2b or r24, r25
81c: 80 93 62 01 sts 0x0162, r24
820: e0 91 62 01 lds r30, 0x0162
824: f0 e0 ldi r31, 0x00 ; 0
826: ed 5f subi r30, 0xFD ; 253
828: fe 4f sbci r31, 0xFE ; 254
82a: 80 81 ld r24, Z
82c: 80 93 80 00 sts 0x0080, r24
830: 2c 98 cbi 0x05, 4 ; 5
832: ff 91 pop r31
834: ef 91 pop r30
836: bf 91 pop r27
838: af 91 pop r26
83a: 9f 91 pop r25
83c: 8f 91 pop r24
83e: 7f 91 pop r23
840: 6f 91 pop r22
842: 5f 91 pop r21
844: 4f 91 pop r20
846: 0f 90 pop r0
848: 0f be out 0x3f, r0 ; 63
84a: 0f 90 pop r0
84c: 1f 90 pop r1
84e: 18 95 reti

アセンブラのソースになった「C」のコードを見ると、
  ここ、どうにかでけへんのか?
というところが見えてきます。

32bitのDDSカウンタ、その上位バイトを取り出して
A相B相出力をアウトプット・コンペアレジスタにセット
するためのテーブルを読み出しているところ、
  1バイトでのシフトで良いのに2バイトで処理してる
  ★Aだけでエエのに。 ★Bはイラんねん。
アセンブラだと、好きに書けるんですが・・・
Cコンパイラの内部処理まで手が出せません。

dds_cntにdds_stepを加算しているところも、使用レジスタを
ケチれば、push/popも減らせます。

dds_cntにunionを使い、32bitのところを8ビットでアクセス
できるようにしたのも高速化のため。
32bitのまま 「dds_cnt >> 26」としたら、えらく時間
がかかってしまいます。

現在の処理時間。
Scope_1123_memo1

パルスだけを見ると「3.5μs」使って処理して
いるように見えますが、ch4のトグルパルスを
見ると、「8μs」ほどかかっています。
割り込みサイクルが12.5μsなので割り込みの
処理時間は65%ほど。
push/popの数を減らせればだいぶと時間稼ぎ
できるんですが。


※追記 : 試してみるものです。

2ビット右シフトのところ、
   b = (udds_cnt.cnt1[3] >> 2) & 0x03; 
>> 2」を「 /4 」にしたら、
   b = (udds_cnt.cnt1[3] / 4) & 0x03; 
1バイトで処理してくれました。

806: 80 91 66 01 lds r24, 0x0166
80a: 86 95 lsr r24
80c: 86 95 lsr r24
80e: 83 70 andi r24, 0x03 ; 3
810: 90 91 7c 01 lds r25, 0x017C
814: 89 2b or r24, r25
816: 80 93 62 01 sts 0x0162, r24

3命令=3クロックだけですが、Cのままで高速化成功!


※補足 今回のDDS処理

・目標周波数 fo (Hz)
・基準周波数 fr = 80kHz
・DDSカウンタ 上位12bit + 下位16bitの28bit
       4096 × 65536
・DDSステップカウンタ step 26bit(max)

step = (fo × (4096×65536)) ÷ fr

1.00Hz出力だと
 step = (1 * (4096×65536)) ÷ 80000
    = 3355
この値を80kHzでDDSカウンタに加算する。

1秒後には80000回加算されて0.999868…Hzの
パルスが出る。

目標周波数が最大の9999.99Hzだと
 step = (9999.99 * (4096×65536)) ÷ 80000
    = 33554398 = 0x01FF FFDE : 25bit
実周波数は、9999.9899…Hzとなる。

A相・B相パルスを4バイトあるDDSカウンタの
上位バイトのbit3とbit2を使って発生させている。
  28bitの上位2ビット

「fr ÷ fo」が「2のn乗」になると、(4096×65536)が
割り切れて加算時の端数が出なくなる。
  例えば 5kHz → 16
     2.5kHz → 32
     1250Hz → 64
     625Hz → 128
     312.5Hz → 256
     156.25Hz → 512
これ以外の時は、特定の周期で出力パルスに
ジッターが生じる。

※ジッターの例
Jj1_20240928090201
0.1秒間とか1秒間の平均なら一定周波数が
カウントされるが、瞬間を見ると主クロック
(今回は80kHz)とのズレが生じている。

上に記した「fr ÷ foが2のn乗」の時だけ、
このズレはなくなる。

※過去記事
  割り込みの高速応答、こんな手法を使ってました。
2020年8月18日:Arduino デューティー計測のためのインプットキャプチャータイミング
2020年8月19日:Arduino インプットキャプチャー割り込みを「ISR_NAKED」で (デューティー比測定)


| | コメント (0)

2024年9月24日 (火)

秋月のI2C接続液晶 AQM1602XAを基板に直付け

SDAとSCLの2線でつなげる液晶モージュールが
なかなか便利です。
先日来使っているのが16文字x2行のこれ。
  AQM1602XA-RN-GBW
液晶の足は1.27mmピッチで一直線。
  AE-AQM1602A(KIT)
これになると2.54mmピッチへの変換基板が付属しています。
内部昇圧回路のコンデンサが付いているので、
主回路への接続は、+電源・GND・SDA・SCLの4本だけ。

ところが、この基板を使うと、液晶を基板に貼り付けるのに
  上に出っぱる
  厚くなる(基板そのものは1.2mm厚)
と、使い勝手がよくありません。
Ll10
Ll13

そこで、こんなつなぎをしています。
足を曲げて外に膨らませ、2.54mmピッチの
ユニバーサル基板にハンダしちゃうのです。

Ll11
ちょい拡大
Ll12

コンデンサの取り付けは基板の裏(パターン側)で。
L14_20240924163601

液晶板と基板の間にはスポンジタイプの両面テープ。
全面に貼ると剥がすとき大ごとになりそうなんで、
両端にちょっとだけ。

この液晶を使い、Arduino UNO R3(のチップで)こんなツールを
作っています。
Ll14

二つともデジタルで周波数を設定できる2相パルス発生回路
です。
90度位相差のあるA相B相パルスを出力します。
CW/CCWとカウント方向を変えられます。

下のは 「fclk / n16」でATmega328の16bitタイマー、
アウトプットコンペアで分周する方式。
周波数を設定できる範囲は0.1Hz~9990.0Hz。
   1/nですんで周波数が高くなると
   ちょいと誤差が出ます。

割り込みの時間に余裕があったんで、パルス数を
指定して「何発出せ」をできるようにしてみました。
  ロータリーエンコーダを回して周波数とパルス数を指定。
  CWあるいはCCWスイッチを押すとパルスを出力。
  設定数出したらSTOP。
2相パルスとして使わなくて、
  「何発かのパルスが欲しい」
ときに便利。
  お手軽ツールで、これってなかなか無いんじゃ
  なかろうか。

上のはDDS方式で、0.01Hz単位で周波数を設定できます。
0.10Hz~9999.99Hz。
こっちは STOP・CW・CCWの操作だけ。

基準カウントクロックが80kHzで、16MHzクロックの
ATmega328にはなかなか重い仕事です。

80kHz=12.5μ秒で割り込みをかけてDDS処理。
タイマー1のコンペアマッチで2相パルスを出力します。
ポートを叩いてH/Lを出力するんじゃないので、ジッターは
ありません。  (DDSに起因するジッタはある)
  80kHzで割り込み。
  DDSのための32bit加算
  次のコンペマッチで出力するA相B相を決定。
これを繰り返します。

※関連記事
2024年6月2日:秋月電子通商のI2C接続液晶表示器
2021年7月2日:秋月の液晶表示器 ACM0802C-NLW-BBW-IIC、I2Cのプルアップ抵抗
2022年7月18日:秋月電子16文字x2行のI2Cインターフェース液晶AQM1602Y
2024年9月12日:液晶表示:CG-RAMの表示がちょいと違う

※重要 液晶へのI2CアクセスとTWI割り込みを切り離せたから
    高速割り込みが安定してできるようになったのです。
2024年4月3日:超低速2相パルス発生回路・ケース入れ
2024年4月7日:I2C液晶のアクセス、割り込み処理で遅れる原因らしきもの
2024年4月12日:I2C液晶のアクセス、割り込みで処理しないようにすると


※2相パルスあれこれ
2024年4月26日:治具作りは面白い:2相パルスの発生
2014年07月01日:高速2相パルス発生回路
2018年5月25日:アナログ入力で2相パルスの周波数と正転逆転を制御する発振器

※2相パルス発生回路 回路図
Cw_ccw2

2022年8月24日:パルスジェネレータを作ってみた:箱に入れた
これの出力はデューティ可変できますが
「パルスを何発欲しい」には対応していません。
設定も「周期」で、「周波数」として合わせたい時は
ちょいと不便。
「全部入り」を作ろうとすると、Ardino UNO-R3ベースでは
だいぶとチカラ不足。

いずれも、電源や表示器以外は、
  ATmega328Pの中に入っている機能だけ
で実現していますんで、
「こんだけできるんやで」と褒めてやりたいところ
です。

※案
DDS方式の2相パルス発生器、割り込みの中では
処理時間の制限でもうなにもできませんが、
「外(main loop)」ではあれこれできそうです。
例えば、周波数を振らす。
  何Hzから何Hzまで、何秒かけて周波数をup/down。
てな処理です。
DDS加算している値を変えると周波数が変わります。
  割り込みでの処理時間は同じ。
設定メニューを出してあれこれでしょう。
  SW4を付けとけば良かった。


| | コメント (0)

2024年9月12日 (木)

液晶表示:CG-RAMの表示がちょいと違う

昔人間ですんで、「これで行こう」と決めたことは
いつまでも引きずっています。
今回のトラブルは液晶表示器の外字登録。

使ったのは秋月で買ったI2C液晶 AQM1602XA
  I2Cで安定して使えるようになって、6本線で
  つなぐ液晶表示器はちょいと縁遠くなりました。

キャラクタ表示の液晶表示器、8文字分のCG-RAMに
外字を登録できます。
文字コードとして0x00~0x07に対応するパターンを書き
込むと、登録した文字が出てくるという仕掛けです。

しかし、その先頭文字が「0x00」となり、C言語の
ヌル終端文字列:Wikipedia に引っかかってしまうので、
文字列の中に入れられません。
実質、7つの外字しか利用できないのです。
そこで、この表にあるよう、
L11_20240912102801

CG-RAMの文字コード0x00~0x07を0x08~0x0Fと
して定義しておくという手法を使ってきました。
  文字列の中に入れずに単独文字として書き込む
  ぶんには0x00で問題なしなんですが

ところが今回のAQM1602XAでこれをすると、定義した
CG-RAMではなく、内蔵された文字が出てきちゃったのです。
L12_20240912102901
16文字×2行の液晶、その下段に0x00~0x0Fの文字を
表示してみました。
L13_20240912103001

左の8つがCG-RAMに入れた登録外字。
昔の液晶なら同じ字が右に8つ並ぶのですが、
この液晶だと、内蔵文字になってしまい、
今までの手法が使えません。

ということで、登録した8つのCG-RAM文字は
0x00~0x07で表示するという、まっとうな処理に
変更しなくちゃなりませんでした。

※関連
PIC AVR 工作室別館 arduinoの館->キャラクターLCD
よく使うパラレル接続液晶(LCDモジュール)のコネクタ信号
液晶表示モジュールを4ビットモードで使ったときの空きピン処理


| | コメント (0)

より以前の記事一覧