RAMの少ないAVRにありがちなこと
- 2023/01/05 12:52
- カテゴリー:make:
年明けにちょっとしたイベントにこれまでに工作したものを作例としていくつか展示することになってしまい,何を出そうか,出すものをそのまま出すか改良して出すかなど,いろいろ考えて年末年始を過ごしました。
出そうと思ったものに,006Pのニッケル水素電池の充電器があります。
https://g-shoes.net/blog/index.php/view/553
もともと自分のためだけに作ってきたものですし,見栄えも良くありませんので,少し体裁を整えてからと思っていたのですが,ところで今って006Pのニッケル水素なんて手に入るのか?と疑問に思い,調べてみました。
結論から言うと入手は問題なく,私が入手していた秋月のGP製も(値上がりしてるけど)大丈夫ですし,ありがたいことに東芝が販売するようになったことから,ヨドバシなどの量販店でも普通に手に入るようになっていました。しかも安い。
入手性から考えると,006Pのニッケル水素と言えばこの東芝のものをさすことが多くなった昨今,これが充電出来ないと意味がないなと充電プロファイルを追加して,Version2への改造を行うことにしました。
秋月のGP製(以下GP)も東芝のものも7セルで8.4Vであることには変わりはないのですが,前者は250mAh,後者は200mAhと結構な違いがあります。
充電の方法も,GP製は0.3C(つまり75mA)で4.5時間に対し,東芝のものは専用充電器の仕様から推測すると0.2C(つまり40mA)で6時間の充電です。
充電電流も時間も異なりますから,結構手の込んだ改造を覚悟しますが,まずは充電電流の対応からです。
この充電器はトランジスタを2つ使った定電流回路で,エミッタ抵抗で電流を決めています。75mAを流すために,最終的にはカットアンドトライで8.32Ω(実測)としてあります。
40mAを流すにはこれに8Ωほどの抵抗を直列に繋ぎ,75mAに切り替えるときにはこの8Ωをショートすることで出来そうです。心配な事は100mA近い電流が流れるのでスイッチが大丈夫かという話と,0.1Ωで大きく電流値が変わる世界なので,スイッチの接触抵抗が影響してこないか,と言う点です。
まあ,素人の工作ですので,セルフクリーニングを期待出来るスライドスイッチで,ちょっと高価な信頼性の高いものでとりあえずやってみます。
実験はとりあえず成功,スイッチの切り替えで電流が75mAと40mAで切り替わります。スイッチの接触抵抗もそうですが,どっちかというと電池スナップの接触抵抗の方が影響が大きいみたいです。
で,このスイッチに2回路のものを使い,位置によってHとLをマイコンに伝達するようにしておきます。それで充電時間を切り替えれば良いだけですので,超簡単!なはずでした。
突っ込まれると思うので先に言い訳しておきますが,電流の切り替えもマイコンで行えば綺麗なのですよ,それにスイッチに直接充電電流を流すというのも気持ちが悪く,正しくはリレーを使うべきでしょう。
0.1ΩオーダーですからトランジスタやFETはオン抵抗が見えてきますし,温度変化もありますのであまり使いたくありません。リレーは信頼性もありますし,電流も流せますから,こういう用途にはぴったりです。
しかし,今回あきらめたのは基板の面積でした。定電流回路の基板は小さく,もう空きスペースがありません。リレーなど全然無理です。
なのでギリギリのサイズとして小型のスライドスイッチを使うことにしたというわけです。
さて,ハードウェアの改造が済んだので,あとはマイコンのソフトの修正です。この時はATTiny2313Aを好んで使っていましたが,幸いなことにPD2が1つ使われずに余っていました。ここを充電電流の設定検出に使いましょう。
ソースをさっと修正,やることは簡単で,スイッチの状態を内部で持っておき,これと実際の状態が変わっていたら状態を更新して,状態にあわせて充電時間をプリセットしたり表示を変えたりするだけの話です。
なにせもともと動いていたソフトです。それも,タイマで1秒ごとに割り込みをかけ,グローバル変数に確保してある時分秒を1秒ずつ減らしていき,ゼロになったら充電完了にするだけのもので,今回はこの部分には手を入れませんから,楽ちん楽ちん。
しかし,そうは問屋が卸しません。
実際に動かしてみると,突然20秒くらい減ったり,1時間ずつ減ったりして,あっという間に充電が終わってしまいます。さらにボタンが効かなくなったりして,もう散々な結果です。
元は10年も前の作品ですし,もしかしたらコンパイラのバージョンが変わったとか,いろいろ考えてみたのですが,元のコードをコンパイルしてみれば問題なく,新しいコードではひどい結果になります。
違いはどこにあるかと調べてみると,RAM使用容量が違っていました。これまでは103バイト,新しいコードは109バイトで,6バイトの違いがありました。しかし使用率は80%台ですし,これがこれほどの違いの原因とは思いませんでした。
しかし,違いは違いです。ちょっと細工をしてRAMを2バイトほど減らしてみると,問題の発生頻度が激減しました。しかし,時間が大きく変わってしまうことは少なくなったとは言え起きてしまうので,使い物になりません。
でも,なんとなく原因はつかめました。対策の効果もあります。ならばその方向に突き進むだけです。と意気揚々と作業を始めますが,もともと128バイトしかないATTiny2313Aですから,空きRAMを数バイト増やすことも至難の業です。
例えば,スライドスイッチの状態を保持する変数は,スイッチの変化を知るには必ず必要な変数です。スライドスイッチそのものが1ビットのメモリだと思えばなんとかなりそうな気もしますが,変化をつかまえるには過去の情報を持つしかなく,それにはメモリが必要です。
1ビットのために1バイト割り当てていたのはもったいないので,ほかのフラグとバインドを試みますが,どうも上手くいかないばかりか,頑張ったところで1バイト減るだけですから,6バイトにはほど遠いです。
そこでスイッチの状態を監視することはやめて,スイッチの状態をなにかのきっかけで調べ,これで充電時間を変えるという割り切りを行いました。スイッチを動かしただけではだめで,起動時かリセット時に取りこまれる仕様にしましたが,これもなかなか使い勝手が良くありません。
関数の呼び出しをやめて直接埋め込むとか,そうした血なまぐさい努力でなんとか106バイトまで減らした結果,上手く動いていそうだったのでこのまま長期試験に突っ込んでみました。すると,20分ほど経過すると充電が終わっています。やっぱりまだダメみたいです。
そもそも,これは時分秒を3つのグローバル変数に割り当ててあり,これが何かの拍子に壊されることで起きている問題です。ならばと,充電のON/OFFを保持している変数を減らすため,タイマのカウントをON/OFFするレジスタを使って充電のON/OFFの状態を確認することにします。
当然ですがなにも問題はなく,1バイト減りました。これでかなり安定してきましたが,それでも時々時間が書き換わってしまいます。
やっぱりもとの103バイトまで減らさないとまずそうです。この段階で105バイト。
ここで私は閃きました。タイマのレジスタで充電ON/OFFを保持出来るなら,他のグローバル変数だってどっかのレジスタで保持出来ないか?
試しに,スライドスイッチの状態を使っていないPORTAのレジスタに書き込んでみました。PORTAは3本しかないので3ビットのメモリとしてしか使えませんが,もともと1ビットあれば十分なフラグですので問題ありません。
これが実に上手く動き,メモリを消費せずに割り切った仕様が復活したので,長期テストにかけました。しかし,やっぱり時々時間が変わってしまいます。
ですが,私はすっかり気をよくしました。他に空いたレジスタを探してみましょう。
時間は最大でも6時間ですので,3ビットあれば十分。ならばUSARTのボーレートの設定レジスタのうち,上位4ビット(UBRRH)使いましょう。分は60秒まで必要ですから下位の8ビット(UBRRL)にします。
残念ながら秒は割り込みハンドラで減算する変数ですので,volatileのグローバル変数でなければならないですから,これはこのままとします。それでも一気に2バイト減っています。ということはもとの103バイトに届いたということです。やったー。
テストを行うと,もう問題は出ません。ちゃんと4時間30分なり6時間までカウントしてくれます。ボタンの操作も問題はありません。
ちょっとしたバグをいくつか潰して完成です。いやー,3日もかかってしまいました。
機能的にはあきらめることなく,思った通りの動作がプログラムできました。
しかしですね,RAMの使用量が103バイトならOKで,105バイトなら何でダメなんですかね,103バイトでもダメな場合だってあるんじゃないか,そもそも原因は何なんだ,というのが,ずっと引っかかっています。
それで,真面目に調べてみました。
まず,ATTiny2313AのRAMはわずか128バイトです。ここに変数やらスタックやら確保されます。使われているRAMのサイズというのは.dataと.bss(と.noinit)の合計ですから,スタックは含まれていません。
更に調べていくと,当たり前の事ですが,スタックはRAMのお尻の方から使われていきます。もしスタックがバンバン使われると,変数の領域を上書きしてしまうでしょう。
mapファイルはリンクマップファイルなのでどのグローバル変数がどのアドレスにあるかはよく分かりません。そこでelfファイルを使って,以下の呪文を唱えます。avr-nm -fsysv -n hoge.elf
こうすると,変数のアドレスが出てきます。素晴らしい。
RAMは0x00800060からですので,時分秒やフラグをグローバル変数に取って109バイトを使ってしまったケースでは,__data_start |00800060| D | NOTYPE| | |.datahour |008000c6| D | OBJECT|00000002| |.datamin |008000c8| D | OBJECT|00000002| |.data__bss_start |008000ca| B | NOTYPE| | |.bss__data_end |008000ca| D | NOTYPE| | |.data_edata |008000ca| D | NOTYPE| | |.datamode |008000ca| B | OBJECT|00000002| |.bsssec |008000cc| B | OBJECT|00000001| |.bss__bss_end |008000cd| B | NOTYPE| | |.bss_end |008000cd| N | NOTYPE| | |.stab
となります。secという秒を保持するグローバル変数は0x008000ccに取られていますし,0x008000cdまではスタックが迫ってきても大丈夫とわかります。
一方で,秒以外のグローバル変数をすべてレジスタに追い出し103バイトに収めたケースでは,__data_start |00800060| D | NOTYPE| | |.data__bss_start |008000c6| B | NOTYPE| | |.bss__data_end |008000c6| D | NOTYPE| | |.data_edata |008000c6| D | NOTYPE| | |.datasec |008000c6| B | OBJECT|00000001| |.bss__bss_end |008000c7| B | NOTYPE| | |.bss_end |008000c7| N | NOTYPE| | |.stab
となり,secのアドレスが0x008000c6に下がり,確かに6バイト分だけ減っています。
それでも,128バイトのRAMのうち109バイトを使っているだけですから,スタックは19バイトも確保出来るはず。これが25バイトになったからといって,どれだけ助かるかという話は,スタックがどれだけ使われるのかを考えないといけなくなります。
しかし,スタックの使用量というのは簡単には計算出来ないものです。関数を再帰的に呼び出せばそれこそ無限にスタックが使われますし,どんな関数でいくつスタックを使うかを知るのはコンパイラだけです。
ただ,割り込みを使うとレジスタの待避でスタックをたくさん使うことは想像できるわけで,gccが吐き出したアセンブルリストを眺めてみました。
まず,気になっていた割り込みハンドラです。やってることは本当に最小限度で,secというグローバル変数をデクリメントしているだけです。ISR(TIMER1_COMPA_vect){ 338: 1f 92 push r1 33a: 0f 92 push r0 33c: 0f b6 in r0, 0x3f ; 63 33e: 0f 92 push r0 340: 11 24 eor r1, r1 342: 8f 93 push r24 sec--; 344: 80 91 cc 00 lds r24, 0x00CC 348: 81 50 subi r24, 0x01 ; 1 34a: 80 93 cc 00 sts 0x00CC, r24} 34e: 8f 91 pop r24 350: 0f 90 pop r0 352: 0f be out 0x3f, r0 ; 63 354: 0f 90 pop r0 356: 1f 90 pop r1 358: 18 95 reti
こんな感じです。スタックの消費量は4つですね。割り込みはこれ以外にないので,多重割り込みは発生しません。
これなら問題ないはずと言いたいところですが,実はこの充電器,LCDに4ビットパラレルの古いものを使っています。しかも悪いことに,LCDの表示に関係するコードは,様々なAVRに対応出来る用に作られたもので,私が作ったものではありません。
ここも確認してみたところ,低レベルの書き込み部分で4つ,これをコールする上位が2つずつで計4つ,合計で8つ使っている感じです。
もしLCDになにか文字列を書き出すときに割り込みがかかったら,12バイトのスタックが詰まれてしまうがわかります。これでも19バイトには届きませんが,なにかあったらアウトになるくらいギリギリです。
正確なところはもう追いかけるのをあきらめましたが,残り105バイトでも異常動作をしたこと,そして103バイトでは正常だったことを思うと,どうもこのあたりに境界があると見て良いでしょう。
結局103バイトで大丈夫という確証を得るのは無理だったわけですが,そこは実績で納得することとし,境界線が105バイトにあるということで,納得することにしました。
AVR,特にRAMが64バイトや128バイトしかないtiny系ではよく問題になることのようで,Cで書くことに慣れてしまうと,ついついこういうミクロな部分を見落としがちになります。
結果が甚大なものになるだけに,小型のマイコンを使う時には気を付けないといけないポイントだと,今さらながらに思った次第です。
アセンブラで書けばまだ意識するんですが,私がAVRを使っている理由はCで実用的なコードが書けるからで,今から全部をアセンブラで書くと言うのもちょっと勇気がありません。
詰まるところ,アセンブラ的にCを使い,十分なテストで乗り切る事が,AVRとの付き合い方かも知れないと思います。