chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

お越しいただきありがとうございます。

このブログでは主に以下の内容を記載しています(自分の記録用)。

  • 何か作った話(自分は何かを作るために生きている..できたらクスっと笑えるものや、アートと絡みたい)(長続きせず、飽きるのが問題)
  • 音楽(AerophoneやDTM(FL-Studio))を勉強しようと思ったが、音楽はレッスンが続けられないので、、絵に転向しようとしたが、DTMに戻った(が、悶々とするだけで何も作っていない)。テクノ音楽を作れるよう、オンライン教材やソフト(AbletonLive等)に投資中。でもLLMに追い越されて、プロンプト入力したら勝手に音楽作ってくれる時代らしい。
  • 趣味のロードバイク(坂道をひーこら走ると結構大変な人生(笑)と重なる。。その後、定年・再雇用でメインタスクからはずれて仕事のストレスがほぼゼロで最近は全く走っていない。。いかん)

記事が渾然一体となってカオス状態ですが、マイコンのプログラムも、ちゃりで走るのも、料理するのも、自分の中ではそれぞれが繋がっているので、カオスな日常をそのまま反映させています。リンクや引用はご自由にどうぞ。質問等ありましたらコメントに書いてください。微力ながら自分の分かる範囲で回答します。
とにかく、、人に「あほやなぁ」と言われる事に精進するのが我が人生。へこんでも創造の力で進みたい

■2025年目標
今年は音楽(EDM)とAI(ミニマムなLLM、エッジAI(tiny ML))に全振りする(時間、お金、そして情熱?)。

■スターを付けてくださる方へのお礼
古い記事を見つけて時々スターを付けてくださる方がおられます。わざわざお越しくださりありがとうございます。不明な点があればコメント欄に書いておいていただけると、回答できるかもしれません。

東雲フォントによるテキスト描画(Hello World)

取り組み:フリーとして公開されている東雲フォントを使ってデジタルTV上でテスト描画
結果:Hello World!は表示できた (レイアウト等はすべて手動)
詳細:

WebPage上で公開されている東雲フォントを入手して、Latin1という8x16のフォントデータを使って英数の文字パターンを整数型配列に変換して、JSONの辞書型に変換、RP2350に転送して、フレームバッファに直書き。puts関数からputc関数を呼び出して、アドレスを計算して、framebufferにORで書き込み(ちょっとバグあるが)
下記のプログラムはRP2350のMicroPython上で動作する、フォントデータを読み込んでFramebufferにフォントデータをORで書き込むサンプル

import json
with open('/lib/latin1.json','r') as f:
    font_list = json.load(f)

def get_font_data(font_list, ascii_code):
    for font in font_list:
        if font['encoding_id'] == ascii_code:
             return font['reverse_bitmap_data']

def get_index(x,y):
    raw_addr = y * 640 + x
    index = int(raw_addr/32)
    offset = raw_addr - index * 32
    return index, offset

def put_char(x, y, c, font_list):
    font = get_font_data(font_list, c)
    for i in range(16):
       draw_y = y + i
       index,offset = get_index(x, draw_y)   
       ptn = font[i]
       value = dvi_vga_mono_demo.disp_frame_buffer[index]
       value |= (ptn << offset)
       dvi_vga_mono_demo.disp_frame_buffer[index]=value

def puts(x,y, str, font_list):
    for i in range(len(str)):
        ascii_code = ord(str[i])
        put_char(x, y, ascii_code, font_list)
        x += 16

GREET='Hello, World!'
y = 100
x = 100
puts(x, y, GREET, font_list)

東雲フォントのLatin1(8x16)によるHello World表示例

横に8dot空いてますが、、詰めるとおかしくなるので多分潜在バグ(アドレス計算にバグがあると思われ)。。。

モノクロのフレームバッファを確保して英数フォントをロードした状態で、空きメモリは100KB程度

>>> gc.mem_free()/1024
102.78125

起動した直後だと480KB空いているのだが

>>> gc.mem_free()/1024
481.09376

■参考URL
使わせてらった東雲(しののめ)フォント(ありがとうございます)
openlab.ring.gr.jp

Spaceblockと温度センサTMP36で水温(氷点下)を計測

背景:理科実験でドライアイスを使って低温(-20度ぐらい?)を作る予定がありどれぐらい低温なのか温度を測りたい
取り組み:タニタの温度計でもいいけど個人の機材を置いて帰らないといけないので、、教室のありもの装置(SpaceBlockか熱電付きのテスタ)で計測したい。センサは安いので買ってみる。TMP36を買った
結果:
詳細:

TMP36だと-30度から125度までの間の温度変化にリニアに反応するらしいが。。

配線は以下

3.3Vの電源共有して、AD変換して、電位を計測する。グラフの式を電位に適用して、温度に変換する。SpaceBlockはLEDパネルとかあるので、そっちに出すか、グラフ描画機能もあるらしい(使い慣れてないので詳細不明)

import time
from machine import ADC, Pin

SENSER_PIN=26
temp_sensor = ADC(Pin(SENSER_PIN))

while True:
    volt = 3.3 * temp_sensor.read_u16() / 65535
    print(volt,'V')
    time.sleep(1)

グラフから読みとって以下の温度変化に当てはめるとする

vol0, temp0 = (0.3, -25)
vol1, temp1 = (1.7, 125))

電圧と温度の関係式は以下

temp = a * vol  + b
a:107.14285714285715
b:-57.14285714285717

Pythonでの温度計測プログラム

import time
from machine import ADC, Pin

SENSER_PIN=26
temp_sensor = ADC(Pin(SENSER_PIN))

CONST_A = 107.14285714285715
CONST_B = -57.14285714285717

def conv_volt_to_temp(volt):
    return CONST_A * volt  + CONST_B

while True:
    volt = 3.3 * temp_sensor.read_u16() / 65535
    print(volt,'V')
    print(conv_volt_to_temp(volt),'C')
    time.sleep(1)

出力例

28.7641832 C
0.801799 V
28.7641832 C
0.8026047 V

だいたいあってそうだが、マイナス20度ぐらいから100度近くまで測って校正したい。

SpaceBlockのブロックプログラミングを実施

3.3Vを最大電圧として、アナログ入力値を計測、近似式に代入して温度を推定
得られた結果をグラフとLEDマトリクスに表示

グラフ表示も行われる

■参考URL
www.analog.com

■追記
SpaceBlockでPythonが使えるようになっていた。これは驚き*1

*1:ぱっと見た感じ、使えるモジュール類はかなり制限がかかっていそう。あとREPLが使えないような印象。MicroPythonの利用もサンドボックス内に制限されるということですね。

RP2450のHSTX/TMDSを使ってDVI信号を生成(最終)

背景:RP2450のHSTX/TMDSを使ってDVI信号を生成して、デジタルTVに画像を出したい
アプローチ:素のMicroPythonのまま、RaspberryPi Pico2を使って、DVI信号を生成する。仕様はモノクロ、画素はVGA(640x480)とする。MicroPythonでDVI信号を生成しようとしても、VGAの描画タイミングに間に合わないのでDMAやPIOで 制御を代用する。
結果:モノクロで640x480画素で表示ができた。また、MicroPythonのframebuf関数が使えるので線やテキストの出力も可能
詳細:
性能を補うためDMAを大量に使って水平同期、垂直同期、TMDSコマンド、画像データをHSTXに送信

モノクロだと1画素1bitとなるので、640x480の画像サイズの場合、framebufferは画像領域だけで約38KBに収まる。PSRAMを搭載しない素のPico2上で実装可能*1
ソースは以下にコミット(変なコメント、間違ったスペル、デバッグコードが残っている)
github.com

DVI信号を出力できるようにRaspberry Pi Pico2にHDMI基板を追加

動作テスト用プログラム (直線、楕円、sin/cosを描画)

import array, framebuf
import math
import time

VGA_WIDTH = 640
VGA_HEIGHT = 480
fb = framebuf.FrameBuffer(disp_frame_buffer, VGA_WIDTH, VGA_HEIGHT, framebuf.MONO_HMSB)


while True:

    #
    # graphics test 0
    #
    
    fb.fill(0)
    fb.text('Hello, world!', 50-1, 50-1,1)
    fb.line(320-1, 0, 320-1, 480-1,1)
    fb.line(0, 240-1, 640-1, 240-1,1)
    fb.line(0, 0, 640-1, 480-1,1)
    fb.line(0, 480-1, 640-1, 0,1)
    fb.ellipse(320-1, 240-1, 240-1, 240-1,1)
    
    time.sleep(3)    
    
    #
    #
    # graphics test 1  (sin/cos)
    #
    
    fb.fill(0)
    fb.text('sin & cos', 50-1, 50-1, 1)
    fb.line(320-1, 0, 320-1, 480-1, 1)
    fb.line(0, 240-1, 640-1, 240-1, 1)
    
    for x in range(640):
      rad = 2 * math.pi * (x - 320) / 320
      y = int(240 +  (240 * math.sin(rad)))
      #print(x,y)
      fb.pixel(x,y,1)
    
      y = int(240 -  (240 * math.cos(rad)))
      #print(x,y)
      fb.pixel(x,y,1)
    
    time.sleep(3)    
    
    #
    #
    #
    #
    
      
    y = 240
    fb.fill(0)
    for i in range(240, 640-240,2):
      x = i
      fb.ellipse(x-2, y, 240-1-2, 240-1,0)
      fb.ellipse(x, y, 240-1, 240-1,1)
    
    time.sleep(3)        
    
    
    
    #
    #
    #

表示例

今後の取り組み
framebufでテキスト表示させると、8x8フォントを使って描画されていると思われる。小さくて読みづらいので、16x16が使えるようにしたい。あと、、、クマのイラストを描画させたい。ソースコードが汚いので、クラス化したりモジュール化したい。今回振り返ると、TMSD Encodeのコマンドの指定方法を最初は誤解していてかなり複雑になってしまった。メモリ領域を消費してよいなら、繰り返しを無くしてメモリ上に全て展開することで、DMAを減らしてかなりシンプルに実装できるはず。
(趣味の)組み込みマイコンでの課題の一つが表示手段と思っていますが、素のRPi Pico2/MicroPythonでもデジタルTVが使えるようになるので、いろいろ便利に使えると期待。ただ、、ちょっと描画速度が遅い、文字が小さい、モノクロなので、時間ができたらカラー化したい(RGB565でQVGA(320x240))。描画の高速化はかなり難しいと思われる。サクッとDVI出力したい場合はCircuitPythonを選ぶべきかも。

■追記
東雲(しののめ)フォント
openlab.ring.gr.jp
東雲フォトのフォーマット
BDF形式といわれるフォーマットらしいですが、かつてはX11等のフォントのフォーマットであったらしい。

STARTCHAR 41
ENCODING 65
SWIDTH 480 0
DWIDTH 8 0
BBX 8 16 0 -2
BITMAP
........
...@....
...@....
..@.@...
..@.@...
..@.@...
.@...@..
.@...@..
.@...@..
.@@@@@..
@.....@.
@.....@.
@.....@.
@.....@.
........
........
ENDCHAR # A

東雲フォントを描画したくなってきた・・・

*1:RGB565で320x240の場合、frame bufferのサイズは150KBになる

RP2350 + MicroPythonでDVI信号を生成する(640x480 モノクロ)-> なんとか動いた->RingバッファのバグをFIX

やりたいこと:RP2350(Raspberry Pi Pico2)を使ってDVI信号を出す (素のMicroPythonを使う、ハードもPico2のままでPSRAMは付けない)
結果:モノクロの640x480のフレームバッファをRP2350のRAM上に構築、HSTX/TMDSを使ってDVI信号を生成、モニタでストライプが出るのを確認、MicroPythonのframebufモジュールでグラフィック表示を確認
詳細:
C言語なら、画像の一行単位で終了割込み入れてアドレス再設定等できるのですが、MicroPythonだと間に合わないので、639行まではDMAのchainで水平同期->画像転送をハードだけで自動的に行い、640行まで転送が終わったかどうかをPIOで判定させて、PIO+DMAで画像転送用DMAのチェーン先を切り替えて、垂直同期用のDMAを起動するようにしています。

当初は320x240のRGB565を考えていましたが、320x240を640x480に変換するのが結構やっかいなので、まずは、画素数変換が不要な640x480モノクロで実装しました。

frameバッファは画素データだけではなく、[TMDS_CTRL][PIXEL(32)].....[TMDS_CTRL][PIXEL(32)]という、制御コマンド+画素データの交互配置(Zipper形式)です。この形式は制御コマンド分が無駄であり、このままだとMicroPythonのframebufによる描画ができないので、Zipper形式の変換もPIOで実装する必要があります。ここはまだできていません。

ソースは以下 (ハードのセットアップが大変なので結構長いプログラムです)
github.com

ストライプを表示したところ

framebufモジュールが使えないので、描画関数を作成。Zipper形式のframe bufferの画素データを書き換え。

VGA_WIDTH = 640
VGA_HEIGHT = 480
PIXEL_PER_WORD = 32

def pset(x, y ,clear_flag=False):
    global disp_frame_buffer
    virtual_address = y * VGA_WIDTH + x
    index = int(virtual_address / PIXEL_PER_WORD)
    index_in_frame_buf = index * 2 + 1   #   2 is zipper format

    pixel_data = disp_frame_buffer[index_in_frame_buf]
    if not clear_flag :
        pixel_data |= 1 << (x % PIXEL_PER_WORD)
    else:
        pixel_data &= ~(1 << (x % PIXEL_PER_WORD))
    disp_frame_buffer[index_in_frame_buf] = pixel_data

for y in (239, 240, 241):
    for x in range(VGA_WIDTH):
         pset(x, y, True)

画面中央に黒い線が入る

ライン描画関数を作ってテスト。

def draw_line(point0, point1,  is_black=False):
    x0, y0 = point0
    x1, y1 = point1
    if x0 == x1:
        for y in range(y0 , y1 + 1):
            pset(x0, y, is_black)
    elif y0 == y1:
        for x in range(x0 , x1 + 1):
            pset(x, y0, is_black)
    else:
        a =  (y0 - y1) / (x0 - x1)
        b = y1 - a * x1
        if a <= 1:
            for x in range(x0 , x1 + 1):
                y = int(a * x + b)
                pset(x, y, is_black)
        else:
            for y in range(y0 , y1 + 1):
                x =  int((y - b) / a)
                pset(x, y, is_black)

線描画してみる

>>> draw_line((0,0),(640,480),is_black=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 16, in draw_line
  File "<stdin>", line 13, in pset
IndexError: array index out of range
>>> draw_line((0,479),(639,0),is_black=True)

0スタートなので、最大位置は(639,479)であった。画像は以下

これ以上描画関数を作るのが大変なので、pythonで扱えるTMDS Controlコマンドを含まない画素だけのframe bufferを作り、そこにframebufモジュールを使って描画する方式を実装する。frame bufferにより出来上がった画素データを1WORD(32bit)単位で転送するが、転送先はZipper形式なので、1 index ずつ飛ばしながらコピーする。framebufの関数を使って円やライン、テキストを描画してみた

import array, framebuf

VGA_WIDTH = 640
VGA_HEIGHT = 480
WORDS_PER_LINE = VGA_WIDTH // 32      
frame_buf = array.array('I', [0] * (WORDS_PER_LINE * VGA_HEIGHT))
fb = framebuf.FrameBuffer(frame_buf, VGA_WIDTH, VGA_HEIGHT, framebuf.MONO_HMSB)


# disp_frame_buffer


#
# dst_fg ... must be zippzer format (*2 of src)
# src_fb ... normal frame buffer
#
def copy_frame_buffer(dst_fb, src_fb):
   if len(src_fb) * 2 != len(dst_fb):
       print('Error in copy_frame_buffer')
       print('not match size (dst is * 2 of src)')
       return False
   for i in range(len(src_fb)):
       dst_fb[i * 2 + 1] = src_fb[i]

   return True


fb.fill(0)
fb.text('Hello, world!', 50-1, 50-1,1)
fb.line(320-1, 0, 320-1, 480-1,1)
fb.line(0, 240-1, 640-1, 240-1,1)
fb.line(0, 0, 640-1, 480-1,1)
fb.line(0, 480-1, 640-1, 0,1)
fb.ellipse(320-1, 240-1, 240-1, 240-1,1)


copy_frame_buffer(disp_frame_buffer, frame_buf)


#
#
#

結果、framebufを使った描画例

やっていること:画像データだけのモノクロframe bufferを作って、MicroPythonのframebufモジュールで描画。出来上がったframebufferのデータをPythonコードでZipper形式のバッファにコピー。(ソフトウエアによるコピー、Zipper形式は、[CMD][画素]が連なってるので、コピーの際インデックスは一つおきに飛ばす必要ある)

sin/cosの描画例

import array, framebuf

VGA_WIDTH = 640
VGA_HEIGHT = 480
WORDS_PER_LINE = VGA_WIDTH // 32      
frame_buf = array.array('I', [0] * (WORDS_PER_LINE * VGA_HEIGHT))
fb = framebuf.FrameBuffer(frame_buf, VGA_WIDTH, VGA_HEIGHT, framebuf.MONO_HMSB)

# disp_frame_buffer
#
# dst_fg ... must be zippzer format (*2 of src)
# src_fb ... normal frame buffer
#
def copy_frame_buffer(dst_fb, src_fb):
   if len(src_fb) * 2 != len(dst_fb):
       print('Error in copy_frame_buffer')
       print('not match size (dst is * 2 of src)')
       return False
   for i in range(len(src_fb)):
       dst_fb[i * 2 + 1] = src_fb[i]

   return True


fb.fill(0)
fb.text('sin & cos', 50-1, 50-1, 1)
fb.line(320-1, 0, 320-1, 480-1, 1)
fb.line(0, 240-1, 640-1, 240-1, 1)

import math
for x in range(640):
  rad = 2 * math.pi * (x - 320) / 320
  y = int(240 +  (240 * math.sin(rad)))
  fb.pixel(x,y,1)
  y = int(240 -  (240 * math.cos(rad)))
  fb.pixel(x,y,1)

copy_frame_buffer(disp_frame_buffer, frame_buf)

#
#
#

画面表示

今後の取り組み
最終的にはZipper形式ではないflame bufferを使ってMicroPythonのframebufで描画させる方式にしたい。そのためには、画素データだけのframe buffer に対して、 Zipper 形式のframe bufferに変換する機構が必要。PIOとDMA2つでできると思ってるけど、ちょっと手間。上記の手抜き方式でも十分な速度なので、もうこれでいいかと・・

余裕ができたらかわいらしいイラストも出してみたい

かわいいくま壁紙モノクロ調|AI百科「シンテリ」で調べる語句・イラスト

■追記
翌日動かしたら、エラーになった。さすがDVI。。簡単には完成しない。Not Supportのメッセージは初めて見ました。同期信号等のタイミングがずれていると思われる。オシロで調べたら分かるかも。

オシロで調べると1画面の周期が71.63Hzであった。これは確かに仕様を逸脱している(正しい周期は60Hz)。

同じプログラムを動かしてるはずなのに、なぜ微妙に狂うのか(苦笑)。。1画面の周期がずれる原因として、画像領域のTMDSエンコーダの指定誤りの可能性が大。

画像1ラインの周期が仕様上は31.68usのはずが、試作版の動作では25.2usで動いている。これはおかしい。一行が早い。クロックは変えていない(つもり)のに、早いということはTMDSエンコーダに対する画素数の指定にミスがあると思われる*1

フレームバッファの先頭2WORDを確認

>>> bin(disp_frame_buffer[0])
'0b10000000100000'
>>> bin(disp_frame_buffer[1])
'0b11110000110011001010101011111111'

解釈すると、、
TMDS:0x2 + size: 0b0010_0000 (0x20) 32画素
32画素分をTMDSエンコードするということなので、実装は意図通り。だとすると、一行あたりのDMA転送サイズに誤りが??
気になるのは、いつもデバッグ時は正常に動くシンプルなソースを動かしてから、ややこしいプログラムのテストを行っていて、正常に動くソースを最初に動かすことで、見えていないパラメータを暗黙のうちに最適に設定している可能性は?? *2
DMA転送パラメータは以下の通りで、これは変えていない

dma2.config(read=buffer, write=HSTX_FIFO_ADDR, ctrl=dma2_ctrl, trigger=False, count=int(VGA_WIDTH/32) *2 )  # /32 means omoncolor,  in 1 data(32) contains 32pixcel,  * 2 means  control + IMAGE

だとすると実行途中にバグ混入で誤って転送サイズを変更してしまっている??
周期をおかしくする要因としては、(1)TMDSエンコードへのパラメータのミス、(2)DMAによるHSTX FIFOへの転送回数のミス、(3)(3)HSTXに供給ししているクロックのミスが挙げられる。エンコードパラメータは合ってそうなので、、転送回数がおかしいか、クロックがおかしいか。転送回数が間違っていないかの判断は、DMAの転送終了時のアドレスがframe bufferの最大アドレスと一致するかを確認すれば良い。
調べるのが楽なシステムクロックを確認、これはOK

HSTXに対してSYSTEM CLOCKを供給している。

# SYS_CLK
#
CLK_ENABLE = 1
CLK_SYS = 0
CLK_PLL_USB=2
val = (CLK_ENABLE << 11) + (CLK_SYS << 5)    # DVI Speed
#val = (CLK_ENABLE << 11) + (CLK_PLL_USB << 5)  # slow speed
write_CLK_HSTX_CTRL(0)  # disable clock
div = 1 << 16
write_CLK_HSTX_DIV(div)   # 150 MHz
write_CLK_HSTX_CTRL(val)  # enable and PLL source

現在のシステムクロックはDVI出力用に調整した126MHzを設定

>>> machine.freq()
126000000

DMA2による転送終了時の番地はデバッグ用に記録していて、 last_frame_addrに格納されている。

>>> last_frame_addr-addressof(disp_frame_buffer)
76800

開始番地から終了番地を引くと、76800アドレス(8bit)となっている。
640x480の画像を出すには、1画素1bitで1WORD 32bitなので、9600WORD必要。Zipper形式で構築しているので、2倍の19200WORD必要。上記76800(8bit)をWORD(32bit)で換算すると、19200WORDとなり一致。だから、転送総数も間違いない。
調べられる範囲では設定は合っている。なのに送信速度がおかしい。だとすると、クロックの解釈が間違っている?
たとえば、DVI画像クロックを調べると、HSTXの動作クロックが正しいわかるはず。
あるいは、計測しているいろんな周期のずれ幅の割合が近い値で揃っているなら、供給しているクロックがずれているともいえる。
DVIのCLKを調べると、12.5MHzであった。仕様では25MHzのはずなのに、なぜ2倍なのか??
正常に動作するシンプル実装で描画させるとエラー無く表示されながら、DIV CLKはやはり12.5MHzであった。なぜこれでいいのか分からないが、、12.5MHzは同じ値なので、描画できなくなった変化点はDVI CLKではない。しかも、25MHzに対してぴったり半分なので、HSTXに供給しているクロックがおかしいわけではない。だとするとあと考えられるのはDMAのDATA REQUEST同期がうまく働かず、時々すっぽ抜けてDMAはHSTXのREADYを待たずにHSTXのFIFOに送ってしまうぐらいではないか。。
Claudeに相談するとDREQを指定していたら、HWの動作上すっぽ抜けることは考えにくく、短縮の比率から考えて、水平同期の送信が抜けたりしていると考えるべきでは??と言ってきた。ふむふむ。
本来の一行の周期に対して短くなっている時間が6us程度、水平同期の表示時間が6.4usなので計算上は合う。水平同期のDMA転送がなんらかの要因で動いていないのか? CTRLレジスタにはERRORフラグが立っていないし、もし全然動いていなかったら、後段のDMAにchain しないだろう。転送回数指定が0になっているとか?

水平同期信号を転送するDMA1の終了割込みで、read addressを取得、すると、バッファの先頭になっていた。

>>> hex(last_hsync_addr)
'0x2000bbd0'
>>> hex(addressof(hsync_frame_buffer))
'0x2000bbd0'

タイミング異常の時、上記の値。見る限り動いていないように思える。RINGだからといって、転送終了した時は、バッファの最後を指しているはず。だから、転送すっぽ抜けている。転送せずに、次のDMAにchainしているように思える。
DMA1の転送先(writeレジスタの値)をデバッグ用に用意したバッファに向ける。hsyncのframe bufferの内容は以下

>>> [hex(x) for x in hsync_frame_buffer]
['0x1008', '0x354d5354', '0x1008', '0x354d5354', '0x1060', '0x354d50ab', '0x1030', '0x354d5354']

最後の値が入るはずだが。。

>>> dma1.write = addressof(tmp_buffer)
>>> tmp_buffer
array('I', [1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
>>> tmp_buffer
array('I', [1, 0, 0, 0, 0, 0, 0, 0, 0, 0])

1とは何か?? 常に1が書き込まれてはいる。
DMA1の転送回数も参照可能。確認すると転送カウント8となっている。(仕様通り)

>>> OFFSET_CH1_DBG_TCR=0x844
>>> print(read_reg(BASE_DMA, OFFSET_CH1_DBG_TCR))
8

まとめると、、HSTXに供給されているクロックは正しい、画像エリア(640x480)の転送回数も正しい、垂直同期、水平同期は呼び出されている。
異常と思われる点: 水平同期信号を送るDMA1の挙動がなにかおかしい印象
DMA1の終了割込みで送信元アドレスを参照すると、送信用データ領域の先頭のアドレスになっている(最後尾のアドレスになるはずでは?)
DMA1の転送において、送信用データ領域の値とは異なる値が送られているような印象。

不具合原因

Claudeにも助けてもらったのだが、、今回の異常動作はRING用に確保した領域のアライメントミスが原因

>>> hex(addressof(hsync_frame_buffer))
'0x2000bbd0'
>>> bin(addressof(hsync_frame_buffer))
'0b100000000000001011101111010000'         # 5bit幅で回したいのに、5bit目が1になっている。NG!

今回のRINGではバス幅として5bitの範囲でぐるぐる回るはずだが、バッファの開始アドレスは、bit5が1になっている。このアドレスから開始したら、正しいリングにならない。だからずれた領域でRING動作した結果、変な値が転送されていた。TMDSでは変な制御コードが来たので無視して取り扱わず、エンコードも速くなったと。。 アライメントを算出しているのは自分の関数なので、原因を作り出したのは自分のアライメント計算関数だ。昨日動いて今日動かないというのも、確保されるアドレスがランダムだから、バグが発生するか、しないか、アドレス次第だったということだろう。。(本当にこれが問題なのか修正完了していないので推測ですが)原因特定まで一日かかったけど、、いやー勉強になった。

アライメントを考慮してバッファを確保する関数
(アライメント不整合のまま返してしまうバグ入り版)

#
# create alligned buffer
# array type is WORD(32bit) only
#
def make_aligned_buffer(size):

    if is_power_of_two(size):
         pass
    else:
         print('internal error! not power of 2')
         return None

    buffer = array('I', [0] * size * 2)  # I means unsigned int
    base_addr = addressof(buffer)
    if base_addr == (base_addr  &  ~(size - 1)):
        target_addr = base_addr
    else:
        target_addr = (base_addr  &  ~(size - 1)) + size
    offset_index = int((target_addr - base_addr) / 4)
    print(offset_index, size)
    return memoryview(buffer)[offset_index : offset_index + size]

alimentの計算において、引数がWORD単位の長さを意図していたのだが、アライメント計算ではメモリ確保に必要なSIZE*4にせず、1/4のWORDのままで計算していた。(sizeの単位を混乱していたのがバグの原因
修正版は以下(単純に*4)

#
# size is n of words (not bytes)
# so in memory area, allocating address is n * 4
#
def make_aligned_buffer(word_size):

    if is_power_of_two(word_size):
         pass
    else:
         print('internal error! not power of 2')
         return None

    buffer = array('I', [0] * word_size * 2)  # 'I'  means unsigned int
    base_addr = addressof(buffer)
    if base_addr == (base_addr  &  ~(word_size * 4 - 1)):
        target_addr = base_addr
    else:
        target_addr = (base_addr  &  ~(word_size * 4 - 1)) + word_size * 4
    offset_index = int((target_addr - base_addr) / 4)
    print(offset_index, word_size)
    return memoryview(buffer)[offset_index : offset_index + word_size]

修正版をcommit
github.com

*1:昨日最後にバグ修正したところだが

*2:過去正常に動作した簡単なテストプログラムは正常に動作し、その後、一番ややこしい作成中のプログラムを動かすとやはりNGであった。再現してよかった(動いてしまうと自分は逃げてしまって、もうデバッグしないだろうから)

RP2350/HSTX/TMDSでDVI信号を出す、モノクロ版

背景:RP2350に搭載されているHSTX(w/ TMDSエンコーダ)を使ってDVI信号を生成してTVでグラフィック描画表示したい
取り組み:VGAサイズでもカラーにするとRP2350のSRAMに乗らないが、モノクロにすると外付けメモリ無くてもSRAM上でフレームバッファを確保することができる。640x480によるフレームバッファ確保に先立ち、HSTX/TMDSでモノクロ出力する設定方法を確認
結果:REPEATを使ってるので、縦じましか出せないが、モノクロ表示を確認
詳細:

ソースコード要約

HSTXのTMDS Expanderの設定。エンコーダへの取り出しは、1bit幅で32回シフトする。(モノクロなので1bitずつ取り出す)

#
# EXPAND SHIFT
#
# EXPAND_SHIFT
# RAW_SHIFT: 32
# RAW_N_SHIFTS: 1

RAW_SHIFT = 0       # 0 means 32
RAW_N_SHIFTS = 1

# encoded expand
# each 1bit (monoral) and 32 times(register setting 0)
#
ENC_SHIFT=1
ENC_N_SHIFTS=0  # N_SHIFT 0 means 32
#
#
val = ENC_N_SHIFTS << 24 | ENC_SHIFT << 16 |RAW_N_SHIFTS << 8 | RAW_SHIFT
write_HSTX_CTRL_EXPAND_SHIFT(val)

3つのTMDSエンコーダにどう渡すのかを指定するHSTX_CTRL:EXPAND_TMDS Registerの設定は以下
25回右ローテートすることでMSBの位置のbit0をbit7まで移動させて、bit幅1(設定値は0)を3レーンのエンコーダに渡す。同じ値を渡しているので、白色か黒色のモノクロになると。。

#
# EXPAND TMDS  MONO COLOR

l0_nbits = 0
l0_rot = 25
l1_nbits = l0_nbits
l1_rot = l0_rot
l2_nbits = l0_nbits
l2_rot = l0_rot

val = l2_nbits << 21 | l2_rot << 16 | l1_nbits << 13 |  l1_rot << 8 |  l0_nbits << 5 |  l0_rot 
write_HSTX_CTRL_EXPAND_TMDS(val)

描画用データは以下で640回リピート。描画パターンは、1111_1111で白い太線で始まり、010101と細かいストライプになるはず。

# mono color
pixel_pattern = 0b1111_0000_1100_1100_1010_1010_1111_1111
                  *略*
        # DISPLAY DATA
        buffer[index] = 0x00_00_30_00 + VGA_WIDTH
        index += 1
        buffer[index] = pixel_pattern
        index += 1
表示確認

改めて見てみると、、出力パターンのLSBから描画されるので、最初は8本分の太い白線から始まる。次に、黒白黒白・・・

ソースコードは以下
github.com

RP2350のDMA転送速度を確認

背景:DMAでいろいろややこしい使い方をしようとしているので、、沼に落ちるのを避けるため簡単なサンプルコード作成と、転送速度を測る
取り組み:まずは1chだけのDMAを使ってGPIOのPinをLHした時の周期を測定
結果:システムクロックの速度(150MHz)でPinをLHできるのかと単純に思っていました。実際計測すると37MHzの速度に留まっていました。これはデータ取り込み等のオーバーヘッドがあるためらしいです*1
詳細:
この方法がベストかどうか分かりませんが、、GPIOのレジスタをDMAでL/H設定する。
テストコード

#
# DMA speed test simple
# dma0 
# dma0  L->H->L->H...    27ns 37MHz
# 
#
from rp2 import DMA
from uctypes import addressof
from array import array

from machine import Pin
pin0 = Pin(0,Pin.OUT)
pin0.low()

BASE_IO_BANK0 = 0x40028000
OFFSET_GPIO0_CTRL = 0x4
GPIO0_CTRL_REG_ADDR = BASE_IO_BANK0 + OFFSET_GPIO0_CTRL

buf_HL = array("I", [0] * 50)
for i in range(0, len(buf_HL), 2):
   buf_HL[i] = (3 << 12) + 5
   buf_HL[i+1] = (2 << 12) + 5

dma0 = DMA()

def dma0_handler(arg):
   print('dmq0 fin')

SIZE_WORD=2
dma0_ctrl = dma0.pack_ctrl(enable = True, size=SIZE_WORD, inc_write=0, inc_read=True, irq_quiet=False)
transfer_count = 10
dma0.irq(dma0_handler)

while True:
    dma0.config(write=GPIO0_CTRL_REG_ADDR, read=buf_HL, count=transfer_count, ctrl=dma0_ctrl, trigger=False)
    print('start address')
    hex(addressof(buf_HL))
    machine.mem32[GPIO0_CTRL_REG_ADDR] = (2 << 12) + 5
    dma0.active(1)
    time.sleep(0.1)
    print('read:', hex(dma0.read))
    print('count:', dma0.count)
    print(dma0.unpack_ctrl(dma0.ctrl))
    
    # dma0  L->H->L->H...    27ns 37MHz

山->谷にかかった時間をオシロで測定

山ー>谷にかかった時間は、27ns (37.04MHz)であった。これが正しい結果なのか?をAIに質問、回答は以下


Claudeに相談すると、データの取り込みやDMAでの送り出しを考慮すると、4サイクル(27ns (37.04MHz))で1データ(32bit)送り出すのは理にかなっているとの回答
■ご参考
別途、DMAを2つ使ってピンポンで送信した場合の速度も調べました。DMA0による送信、DMA0->DMA1の切り替え、DMA1の送信までの時間を測ると、47ns(21.28MHz)でした。ですので単純に引き算で計算すると、2つのDMA間での切り替えにかかるオーバーヘッドは、20nsで、約3サイクルかかっている計算になります。

2つのDMAをピンポンで動かすサンプルコード

#
# DMA speed test
# dma0 <--> dma1  pin pon  archi tecture
#
# dma0 -> dma1   pin pon  21.28MHz  47ns  (time for H->L)
#

from rp2 import DMA
from uctypes import addressof
from array import array

BASE_IO_BANK0 = 0x40028000
OFFSET_GPIO0_CTRL = 0x4
GPIO0_CTRL_REG_ADDR = BASE_IO_BANK0 + OFFSET_GPIO0_CTRL

from machine import Pin
pin0 = Pin(0,Pin.OUT)
pin0.low()

# data set for GPIO to H and GPIO to L
buf_H = array("I", ((3 << 12) + 5,))
buf_L = array("I", ((2 << 12) + 5,))

dma0 = DMA()
dma1 = DMA()

# data size (WORD)
TRANSFER_SIZE_WORD = 2
transfer_size = TRANSFER_SIZE_WORD 

# transfer count
transfer_count = 1

dma0_ctrl = dma0.pack_ctrl(enable = True, size=transfer_size, inc_write=0, inc_read=0, irq_quiet=True, chain_to=dma1.channel)
dma0.config(write=GPIO0_CTRL_REG_ADDR, read=buf_H, count=transfer_count, ctrl=dma0_ctrl, trigger=False)

dma1_ctrl = dma1.pack_ctrl(enable = True, size=transfer_size, inc_write=0, inc_read=0, irq_quiet=True, chain_to=dma0.channel)
dma1.config(write=GPIO0_CTRL_REG_ADDR, read=buf_L, count=transfer_count, ctrl=dma1_ctrl, trigger=False)

dma0.active(1)

*1:自分はDMAって一度走り出すと、パイプラインにように詰め詰めで転送してくれるものと単純に想像していました