بيثون و FPGA. تجريب

متابعةً للمقال الأول ، أريد أن أوضح على سبيل المثال خيارًا للعمل مع FPGA (FPGA) في بيثون. سوف تغطي هذه المقالة جانب الاختبار بمزيد من التفاصيل. إذا كان إطار MyHDL يسمح للأشخاص الذين يعملون على بيثون ، مستخدمين بناء الجملة والنظام الإيكولوجيين المألوفين ، بالنظر إلى عالم FPGA ، فإن مطوري FPGA ذوي الخبرة لا يفهمون معنى استخدام الثعبان. تتشابه نماذج وصف الأجهزة الخاصة بـ MyHDL و Verilog ، ويعد اختيار لغة معينة أمرًا معتادًا والذوق. يرمز Verilog / VHDL إلى حقيقة أن البرامج الثابتة تمت كتابتها بهذه اللغات لفترة طويلة ، وهي في الواقع قياسية لوصف المعدات الرقمية. بيثون ، كمبتدئ في هذا المجال ، يمكن أن تتنافس في كتابة بيئات الاختبار. يقضي جزء كبير من وقت مطور FPGA في اختبار تصاميمه. بعد ذلك ، أريد أن أوضح مع مثال كيف يتم ذلك في بيثون مع MyHDL.

افترض أن هناك مهمة لوصف جهاز يعمل مع الذاكرة على FPGA. من أجل البساطة ، سآخذ الذاكرة التي تتصل بالأجهزة الأخرى من خلال واجهة متوازية (وليس من خلال واجهة تسلسلية ، على سبيل المثال ، I2C). مثل هذه الدوائر الصغيرة ليست عملية دائمًا نظرًا لحقيقة أن العديد من المسامير مطلوبة للعمل معها ؛ ومن ناحية أخرى ، يتم توفير تبادل أسرع وأسهل للمعلومات. على سبيل المثال ، 1645RU1U المحلية ونظائرها.



وصف الوحدة


يبدو السجل كما يلي: FPGA يعطي عنوان خلية 16 بت ، بيانات 8 بت ، يولد إشارة كتابة WE (تمكين الكتابة). منذ تمكين OE (تمكين الإخراج) و CE (تمكين الشريحة) دائمًا ، تحدث القراءة عند تغيير عنوان الخلية. يمكن القيام بالكتابة والقراءة بالتتابع في عدة خلايا متتالية ، بدءًا من عنوان adr_start محدد ، يتم تسجيله على الحافة الأمامية لإشارة adr_write ، وخلية واحدة في عنوان تعسفي (وصول عشوائي).

في MyHDL ، يبدو الكود هكذا (تأتي إشارات الكتابة والقراءة في المنطق العكسي):

from myhdl import * @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): #     mem_z = data_memory.driver() #      @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: #    adr.next = adr_start else: #    / adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 0 #    ,    else: mem_z.next = None #        data_out.next = data_memory we.next = 1 return write_data, write_start_adr 

إذا تم تحويلها إلى Verilog باستخدام الوظيفة:

 def convert(hdl): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(0)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] inst = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) inst.convert(hdl=hdl) convert(hdl='Verilog') 

ثم نحصل على ما يلي:
 `timescale 1ns/10ps module ram_driver ( data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we ); input [7:0] data_in; output [7:0] data_out; reg [7:0] data_out; output [15:0] adr; reg [15:0] adr; input [15:0] adr_start; input adr_write; inout [7:0] data_memory; wire [7:0] data_memory; input read; input write; output we; reg we; reg [7:0] mem_z; assign data_memory = mem_z; always @(write) begin: RAM_DRIVER_WRITE_DATA if ((!write)) begin mem_z <= data_in; we <= 0; end else begin mem_z <= 'bz; data_out <= data_memory; we <= 1; end end always @(posedge adr_write, posedge write, negedge read) begin: RAM_DRIVER_WRITE_START_ADR if (adr_write) begin adr <= adr_start; end else begin adr <= (adr + 1); end end endmodule 

ليس من الضروري تحويل مشروع إلى Verilog للمحاكاة ، فستكون هذه الخطوة مطلوبة لفلاش FPGA.

النمذجة


بعد وصف المنطق ، يجب التحقق من المشروع. يمكنك تقييد نفسك ، على سبيل المثال ، لمحاكاة تأثيرات المدخلات ورؤية استجابة الوحدة في مخطط الوقت. ولكن مع هذا الخيار ، يكون من الصعب التنبؤ بتفاعل الوحدة النمطية الخاصة بك مع شريحة ذاكرة. لذلك ، للتحقق الكامل من تشغيل الجهاز الذي تم إنشاؤه ، تحتاج إلى إنشاء نموذج ذاكرة واختبار التفاعل بين هذين الجهازين.

نظرًا لأن العمل يتم في python ، فإن القاموس المعين (القاموس) يشير إلى نفسه لطراز الذاكرة. البيانات التي يتم تخزينها على أنها {key: value} ، وفي هذه الحالة {address: data}.

 memory = { 0: 123, 1: 456, 2: 789 } memory[0] >> 123 memory[1] >> 456 

للغرض نفسه ، يكون نوع بيانات القائمة مناسبًا ، حيث يكون لكل عنصر إحداثياته ​​الخاصة التي تشير إلى موقع العنصر في القائمة:

 memory = [123, 456, 789] memory[0] >> 123 memory[1] >> 456 

يبدو أن استخدام القواميس لمحاكاة الذاكرة هو الأفضل بالنظر إلى زيادة وضوح الرؤية.

يبدأ وصف shell test (في الملف test_seq_access.py) بالإعلان عن الإشارات وتهيئة الحالات الأولية ورميها في وظيفة برنامج تشغيل الذاكرة الموضح أعلاه:

 @block def testbench(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) 

فيما يلي وصف لنموذج الذاكرة. تتم تهيئة الحالات الأولية ، افتراضيًا ، يتم ملء الذاكرة بقيم صفرية. قصر طراز الذاكرة على 128 خلية:

 memory = {i: intbv(0) for i in range(128)} 

ووصف سلوك الذاكرة: عندما نكون في الحالة المنخفضة ، اكتب القيمة في السطر إلى عنوان الذاكرة المقابل ، وإلا فإن النموذج يعطي القيمة في العنوان المحدد:

 mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None 

ثم ، في نفس الوظيفة ، يمكنك وصف سلوك إشارات الإدخال (في حالة الكتابة / القراءة المتسلسلة): يتم تسجيل عنوان البداية ← تسجيل 8 خلايا معلومات ← تسجيل العنوان ← قراءة 8 خلايا معلومات مسجلة.

 @instance def stimul(): init_adr = random.randint(0, 50) #   yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr #   yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # 8    write.next = 0 data_in.next = random.randint(0, 100) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr #   adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): #   read.next = 0 yield delay(100) read.next = 1 yield delay(100) raise StopSimulation return stimul, ram, access 

تشغيل المحاكاة:

 tb = testbench() tb.config_sim(trace=True) tb.run_sim() 

بعد بدء تشغيل البرنامج ، يتم إنشاء ملف testbench_seq_access.vcd في مجلد العمل ، وفتحه في gtkwave:

 gtkwave testbench_seq_access.vcd 

ونحن نرى الصورة:



تم قراءة المعلومات المسجلة بنجاح.

يمكنك مشاهدة محتويات الذاكرة عن طريق إضافة التعليمات البرمجية التالية إلى testbench:

 for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) 

يظهر التالي في وحدة التحكم:



اختبار


بعد ذلك ، يمكنك إجراء العديد من الاختبارات الآلية مع زيادة عدد الخلايا القابلة للقراءة / القابلة للقراءة. للقيام بذلك ، تتم إضافة عدة دورات اختبار وقواميس وهمية إلى testbench ، حيث تتم إضافة المعلومات المكتوبة والقابلة للقراءة والتأكيد ، مما يؤدي إلى حدوث خطأ إذا كان القواميس غير متساوين:

 @instance def stimul(): for time in range(100): temp_mem_write = {} temp_mem_read = {} init_adr = random.randint(0, 50) yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[i] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): read.next = 0 temp_mem_read[i] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, "   " for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) raise StopSimulation return stimul, ram, access 

بعد ذلك ، يمكنك إنشاء اختبار آخر لاختبار العملية في وضع الوصول العشوائي: test_random_access.py.

تتشابه فكرة الاختبار الثاني: نكتب معلومات عشوائية على عنوان عشوائي ونضيف زوجين {address: data} إلى قاموس temp_mem_write. ثم نلتف حول العناوين الموجودة في هذا القاموس ونقرأ المعلومات من الذاكرة ، ندخلها في قاموس temp_mem_read. وفي النهاية مع بناء التأكيد ، نتحقق من محتويات القواميس.

 import random from myhdl import * from ram_driver import ram_driver @block def testbench_random_access(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) memory ={i:intbv(0) for i in range(128)} mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None @instance def stimul(): for time in range(10): temp_mem_write = {} temp_mem_read = {} yield delay(100) for i in range(64): write.next = 1 adr_write.next = 1 adr_start.next = random.randint(0, 126) yield delay(100) adr_write.next = 0 yield delay(100) write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[int(adr_start.val)] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) for key in temp_mem_write.keys(): adr_start.next = key adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) read.next = 0 temp_mem_read[key] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, '  random access' raise StopSimulation return stimul, ram, access tb = testbench_random_access() tb.config_sim(trace=True) tb.run_sim() 

لدى Python عدة أطر لأتمتة تنفيذ الاختبار. سوف آخذ pytest للبساطة ، يجب أن يتم تثبيته من نقطة:

 pip3 install pytest 

عندما يتم تشغيل الأمر "pysest" من وحدة التحكم ، فإن إطار العمل سوف يجد وينفذ كل الملفات في مجلد العمل مع "test_ *" بأسمائهم.



تم الانتهاء من الاختبارات بنجاح. سأخطئ في وصف الجهاز:

 @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): mem_z = data_memory.driver() @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: adr.next = adr_start else: adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 1 #  ,    else: mem_z.next = None data_out.next = data_memory we.next = 1 

أجري الاختبارات:



كما هو متوقع ، في كلا الاختبارين ، تم اعتبار المعلومات الأولية (الأصفار) ، أي أنه لم يتم تسجيل معلومات جديدة.

الخاتمة


يتيح لك استخدام python مع myHDL أتمتة اختبار البرامج الثابتة المطورة من أجل FPGAs وإنشاء أي بيئة اختبار تقريبًا باستخدام الإمكانات الغنية من لغة برمجة python.

يعتبر المقال:

  • إنشاء وحدة نمطية تعمل مع الذاكرة ؛
  • إنشاء نموذج الذاكرة ؛
  • خلق حالة الاختبار ؛
  • اختبار الأتمتة مع إطار pytest.

Source: https://habr.com/ru/post/ar442010/


All Articles