Raspberry Pi Pico W jetzt mit Bluetooth - Teil 4 - Robot Car mit Blaulicht und Sirene (PIO-gesteuert) - AZ-Delivery

Raspberry Pi Pico W jetzt mit Bluetooth Teil 4 - Robot Car mit Blaulicht und Sirene (PIO-gesteuert)

Im letzten Teil hatten wir gesehen, dass man den Raspberry Pi Pico W mit Bluetooth mit dem Smartphone steuern kann. Dazu haben wir einen Code verwendet, der eine „signed Integer“-Zahl (max. 2 hoch 15 -1 = 32767) sendet. Die ersten vier Stellen werden für die Steuerung verwendet, die letzte Stelle können wir für insgesamt drei Schalter/Taster verwenden, um z.B. eine blaue LED blinken zu lassen, eine Sirene einschalten, oder umschalten zwischen Fernsteuerung und autonomem Modus.

Seit Einführung des Pico nehmen die Beispiele und Erklärungen zu PIO (Programmable I/O) einen breiten Raum in der Dokumentation ein. Offensichtlich ein Alleinstellungsmerkmal des Pico gegenüber anderen Mikrocontrollern. Hiermit kann man kleine Programme laufen lassen, ohne die CPU zu belasten. Genau das Richtige für das Blaulicht und die Sirene meines Robot Cars. Aber wie macht man das? Die einfachen Beispiele in der minimalen Assembler-Sprache konnte ich nachvollziehen, aber zwei unabhängige StateMachines in den Code für mein Robot Car einzubauen hat ein Weilchen gedauert. Sie würden diese Zeilen nicht lesen, wenn es am Ende nicht doch noch gelungen wäre.

Verwendete Hardware

1

Raspberry Pi Pico W mit aktueller Firmware

1

Smart Robot Car Kit

alt

beliebiger Bausatz mit Chassis und Rädern/Motoren 

1

L298N Motor Controller Board mit DC-DC-Converter 5V

1

Breadboard, Jumperkabel, Batteriekasten mit 4 AA-Batterien

1

LED mit Vorwiderstand, passiver (!) Buzzer

PC mit Thonny, Android Smartphone/Tablet

Smartphone


Smartphone App

Hier zunächst der Link zu dem Code für die Smartphone App, den man im MIT App Inventor importieren kann, um ihn ggf. für eigene Zwecke anzupassen. Denken Sie daran, dass die UUID (zumindest im Umkreis von mehreren Hundert Metern) „unique=einzigartig“ sein muss, also ggf. neue anfordern und sowohl in der App als auch im MicroPython-Code ändern.

Nach diesen Änderungen kann man entweder die App provisorisch mit dem AI Companion laden oder unter dem Menüpunkt „Build“ als Android App kompilieren und hochladen. (Apple Nutzer bitte im Internet recherchieren, wie das beim iPhone funktioniert).

Screen Shot vor dem Verbinden,
Pico W ist „advertising“

Screen Shot
Smartphone ist mit dem Pico verbunden

Quellcode in MicroPython

Nun zu den MicroPython-Programmen, die einerseits die Steuerung des Robot Cars und andererseits die neuen Funktionen realisieren müssen. Dabei tasten wir uns anhand der Beispiele der Raspberry Pi-Foundation und ihrer Zeitschriften an das Thema PIO heran. Diese Programmteile ergänzen später das im vorherigen Teil verwendete Programm.

Häufig findet man das PIO-Assembly-Language-Programm für die eingebaute LED (beim Pico ohne W!), also Pin 25.

# Example using PIO to blink an LED and raise an IRQ at 1Hz.
import time
from machine import Pin
import rp2
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink_1hz():
   # Cycles: 1 + 1 + 6 + 32 * (30 + 1) = 1000
   irq(rel(0))
   set(pins, 1)
   set(x, 31) [5]
   label("delay_high")
   nop() [29]
   jmp(x_dec, "delay_high")
   # Cycles: 1 + 1 + 6 + 32 * (30 + 1) = 1000
   nop()
   set(pins, 0)
   set(x, 31) [5]
   label("delay_low")
   nop() [29]
   jmp(x_dec, "delay_low")

# Create the StateMachine with the blink_1hz program, outputting on Pin(25).
sm = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(25))
# Set the IRQ handler to print the millisecond timestamp.
sm.irq(lambda p: print(time.ticks_ms()))
# Start the StateMachine.
sm.active(1)

Wenig überraschend muss man zu Beginn Module oder Teile davon importieren:

import time
from machine import Pin
import rp2

Dabei ist rp2 ein Mikrocontroller-spezifisches Modul für den Pico, die anderen werden als bekannt vorausgesetzt. Beim Aufruf der Methoden dieses Moduls muss „rp2.“ vorangestellt werden. Alternativ hätte man den folgenden Code verwenden können:

from rp2 import PIO, StateMachine, asm_pio

Als nächstes betrachten wir die Zeilen

@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)

und

sm = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(25))

Zunächst verwenden wir den @asm_pio-decorator, um MicroPython darüber zu informieren, dass eine Methode in PIO-Assembly-Language geschrieben ist. Wir wollen die Methode set_init verwenden, um einen GPIO als Ausgang zu verwenden. Dabei wird zunächst ein Parameter übergeben, um ihm den Anfangszustand des Pins (LOW) mitzuteilen.

Dann instanziieren wir die StateMachine und übergeben dabei einige Parameter

  • die Nummer der StateMachine (es gibt acht, also Nr. 0 bis 7)
  • das Programm, das die StateMachine ausführen soll (hier blink_1hz, ohne Klammern)
  • die Frequenz, mit der wir es laufen lassen wollen
  • der Pin, auf den der Set-Befehl wirken soll (hier die eingebaute LED im Pico ohne Wifi/BT)

Die Maschinensprache PIO-Assembly kennt nur wenige Befehle, die wir z.T. in der selbstdefinierten Funktion blink_1hz() erkennen. Jeder Befehl dauert einen Taktzyklus, die Zahlen in eckigen Klammern stehen für weitere angehängte Zyklen. Bei der gewählten Frequenz von 2000Hz sollen jeweils 1000 Zyklen für den ein- und ausgeschalteten Zustand der LED aufgewendet werden, das ermöglicht das Blinken im Sekundentakt.

Der Interrupt Request ( irq(rel(0)) ) wird im Hauptprogramm verwendet, um einen Zeitstempel auszugeben.

Der set-Befehl wird in zweierlei Weise verwendet:

    set(pins, 1)
   #bzw.
   set(pins, 0)
   
   #oder
   set(x, 31) [5]

Einmal wird der bei der Instanziierung festgelegte Pin auf 1 (HIGH) bzw. 0 (LOW) gesetzt;
im zweiten Fall wird eines der Scratch-Register mit Namen x auf 31 mit anschließend [5] (Warte-) Zyklen gesetzt. Die Ergänzung [5] wird als „instruction modifier“ bezeichnet (s.u.)

Mit den Anweisungen

label("delay_high")   bzw.    label("delay_low")

werden Sprungadressen für die Warteschleifen definiert, auf die der Jump-Befehl - jmp(condition, label) - verweist. Dieser wird so lange ausgeführt, bis x gleich 0 wird. Anfangswert war 31 (s.o.), mit jedem Durchgang wird der Wert mit dem ersten Argument jmp(x_dec, "delay_low") um 1 vermindert („decrement“).

An dieser Stelle macht es Sinn, sämtliche Befehle der Maschinensprache PIO-Assembly und die weiteren „Sprungbedingungen“ (condition) sowie zum besseren Verständnis die Register der StateMachine vorzustellen, denn die meisten Bedingungen sind an die Scratch-Register X und Y sowie das Output Shift Register (OSR) geknüpft:

Hier zunächst die neun “instructions” mit kurzer Erklärung:

  • jmp() - Sprungbefehl, transferiert die Kontrolle an eine andere Stelle im Code
  • wait() - Programmablauf pausiert bis ein vorbestimmtes Ereignis eintritt
  • in_() - Bit-Shift von einer Quelle (Scratch Register or Pins) in das Input Shift Register (ISR)
  • out() - Bit-Shift vom Output Shift Register (OSR) zu einem Ziel
  • push() - sendet Daten zum RX FIFO
  • pull() - empfängt Daten vom TX FIFO
  • mov() verschiebt Data von einer Quelle zu einem Ziel
  • irq() setzt oder löscht die IRQ flag (Interrupt)
  • set() schreibt einen Wert in ein Ziel (Register oder Pin)

StateMachine („Zustandsmaschine“) mit Schieberegistern ISR und OSR, Scratch-Registern x und y, dem Clock-Divider, den Program Counter (im Bild mit PC abgekürzt), und der Kontroll-Logik
(Bild: Raspberry Pi Foundation)

Und die Sprungbedingungen für jmp:

  • (no condition) Sprungbefehl ohne Bedingungen
  • not_x Sprungbefehl, wenn x == 0
  • x_dec Decrement x (minus 1) und Sprungbefehl, wenn x != 0
  • not_y Sprungbefehl, wenn y == 0
  • y_dec Decrement y (minus 1) und Sprungbefehl, wenn y != 0
  • x_not_y Sprungbefehl, wenn x != y
  • pin Sprungbefehl, wenn Pin ist “not low”
  • not_osr Sprungbefehl, wenn Output Shift Register (OSR) nicht leer ist

Zurück zu unserem Robot Car. Das soll nun ein blaues Blinklicht bekommen, wenn in der APP auf dem Smartphone der erste Button (Schaltfläche oben links) gedrückt wird.

Die drei Buttons haben die Wertigkeiten 1, 2 und 4. Die Summe der eingeschalteten Buttons ergibt die letzte Stelle unseres fünfstelligen Codes. Dieser wird wie folgt in seine Bestandteile zerlegt:

code = str(code)[2:-5]  # necessary only for Smartphone APP
code = int(code)
cy = int(code/1000)             # digit 1 and 2   # für Vorwärts/Rückwärts
cx = int((code-1000*cy)/10)     # digit 3 and 4   # für Links/Rechts
cb = code - 1000*cy - 10*cx     # digit 5   # für gewählte Schaltflächen

Welche Schaltfläche(n) gedrückt ist (sind), ergibt sich aus den if-Abfragen von cb & Zahl:

if cb & 1 == 1: # 1. Schaltfläche
if cb & 2 == 2: # 2. Schaltfläche
if cb & 4 == 4: # 3. Schaltfläche

Durch die UND-Verknüpfung & werden auch mehrere gedrückte Schaltflächen erkannt. Die dritte Schaltfläche (Wertigkeit 4) ist Platzhalter für die Umschaltung zwischen Fernsteuerung und autonomen Modus (noch nicht implementiert).

Wie oben beschrieben, instanziieren wir die StateMachine 0 mit der Zeile

sm0 = rp2.StateMachine(0, blink_1hz, freq=2000, set_base=Pin(14))

Dabei wird die blaue LED mit einem Vorwiderstand von ca. 200 Ohm an GPIO 14 (phys. Pin 19) angeschlossen. Mit

if cb & 1 == 1:
   print("blink")
   sm0.active(1)
else:
   sm0.active(0

wird die StateMachine bei cb & 1 == 1 (wegen der &-Verknüpfung auch bei 3, 5 und 7) aktiviert, anderenfalls deaktiviert.

Im nächsten Schritt möchte ich einen Buzzer als Sirene hinzufügen. Im ersten Versuch nehme ich einen aktiven Buzzer und schließe diesen an Pin 16 an. Nach vielen Irrwegen begnüge ich mich damit, dass auch der Buzzer im Sekundentakt tönt, also auch das PIO-Programm blink_1hz verwendet. Allerdings müssen wir einen zweiten Pin „anmelden“ und diesen im @asm_pio-decorator ergänzen:

@rp2.asm_pio(set_init=(rp2.PIO.OUT_LOW, rp2.PIO.OUT_LOW))   # für zwei Pins (beide OUT und LOW)

Dann instanziieren wir eine zweite StateMachine sm1 mit GPIO 16 (phys. Pin 21)

sm1 = rp2.StateMachine(1, blink_1hz, freq=2000, set_base=Pin(16))

denn die Sirene soll beim Drücken der zweiten Schaltfläche ein- bzw. ausgeschaltet werden.

if cb & 2 == 2:          # True auch bei cb==4 oder cb==6
   print("active Buzzer ebenfalls blink_1hz")
   sm1.active(1)
else:
   sm1.active(0)

Mein Versuch, eine zweite PIO-Funktion in Anlehnung an die erste Funktion blink_1hz zu programmieren, endete mit Fehlermeldungen, die ich nicht aufklären konnte. Also erst einmal zufrieden mit dem Erreichten sein und eine Pause einlegen.

Von anderen Mikrocontrollern kenne ich die Möglichkeit, bei Verwendung eines passiven Buzzers unterschiedliche Töne zu erzeugen. Vielleicht hat das schon jemand mit dem Pico und PIO probiert?
Also Suchmaschine anschmeißen und neben „Raspberry Pi Pico“ auch „PIO“ und „Töne“ verknüpfen. Bei Ben Everard, dem Chefredakteur der Zeitschrift „Hackspace“, werde ich fündig.

Er hat ein MicroPython-Modul mit Namen PIOBeep.py und ein Demo-Programm mit dem Lied Happy Birthday geschrieben. Hört sich schauderhaft an, aber man erkennt die Melodie. Ich möchte in Anlehnung an das Martin-Horn nur die Frequenzen 440Hz (Kammerton a) und 587 Hz (das darüberliegende d) verwenden, entscheide mich jedoch für die Tonfolge „tu-ta-ta-tu“, um Verwechslungen mit den Rettungsfahrzeugen auszuschließen. Hier zunächst der Code für das Modul, bei dem wir weitere Elemente der PIO-Assembly-Language kennenlernen werden.
Quelle: https://github.com/benevpi/pico_pio_buzz

from machine import Pin
from rp2 import PIO, StateMachine, asm_pio
from time import sleep

max_count = 5000
freq = 1000000

#based on the PWM example.
@asm_pio(sideset_init=PIO.OUT_LOW)
def square_prog():
   label("restart")
   pull(noblock) .side(0)
   mov(x, osr)
   mov(y, isr)
   
   #start loop
   #here, the pin is low, and it will count down y
   #until y=x, then put the pin high and jump to the next section
   label("uploop")
   jmp(x_not_y, "skip_up")
   nop()         .side(1)
   jmp("down")
   label("skip_up")
   jmp(y_dec, "uploop")
   
   #mirror the above loop, but with the pin high to form the second
   #half of the square wave
   label("down")
   mov(y, isr)
   label("down_loop")
   jmp(x_not_y, "skip_down")
   nop() .side(0)
   jmp("restart")
   label("skip_down")
   jmp(y_dec, "down_loop")
   
class PIOBeep:
   def __init__(self, sm_id, pin):
   
       self.square_sm = StateMachine(0, square_prog, freq=freq, sideset_base=Pin(pin))

       #pre-load the isr with the value of max_count
       self.square_sm.put(max_count)
       self.square_sm.exec("pull()")
       self.square_sm.exec("mov(isr, osr)")

   #note - based on current values of max_count and freq
   # this will be slightly out because of the initial mov instructions,
   #but that should only have an effect at very high frequencies
   def calc_pitch(self, hertz):
       return int( -1 * (((1000000/hertz) -20000)/4))
   
   def play_value(self, note_len, pause_len, val):
       self.square_sm.active(1)
       self.square_sm.put(val)
       sleep(note_len)
       self.square_sm.active(0)
       sleep(pause_len)
       
   def play_pitch(self, note_len, pause_len, pitch):
       self.play_value(note_len, pause_len, self.calc_pitch(pitch))

Mit der Verlagerung des Programmcodes in ein Modul ist dieser leichter portierbar in andere Anwendungen. Die Grundidee ist Verwendung von Pulsweiten-Modulation (PWM) mit 50% Duty Cycle, also eine Rechteckspannung mit gleichen Ein- und Ausschaltzeiten.

Unterschiede zum vorherigen PIO-Assembly-Programm:

Mit dem Importieren der rp2-Modulteile durch

from rp2 import PIO, StateMachine, asm_pio

kann man beim Aufruf von PIO, StateMachine und asm_pio auf das vorangestellt rp2. verzichten.

Beim @asm_pio-decorator und in der folgenden PIO-Assembler-Funktion werden anstelle von set_init und set() für das Schalten der GPIO-Pins sideset_init und .side() verwendet.

Während set() einer der neun Befehle (s.o.) ist, wird die Anweisung .side() als „instruction modifier“ bezeichnet und an einen anderen Befehl angehängt.

Die “instruction modifiers” sind:

  • .side() - setzt den/die side-set pins zu Beginn der Anweisung
  • [number] Warteschleife Anzahl Zyklen nach Ende der Anweisung

Noch ein Wort zu nop(): Diese Anweisung gehört nicht zu den neun Befehlen und wird als  „Pseudo Instruction“ bezeichnet, die als mov(y, y) assembliert wird und nichts bewirkt.

In meinem MicroPython-Programm muss ich nun zusätzlich das Modul PIOBeep (das selbstverständlich auf dem Pico, ggf. im Unterverzeichnis lib, abgespeichert werden muss) importieren und weitere Zeilen aus dem Beispiel kopieren.

import PIOBeep
beeper = PIOBeep.PIOBeep(0,16)
# frequencies of the notes, standard pitch (Kammerton a) is notes[5]=440 Hz
notes = [261, 293, 330, 349, 392, 440, 494, 523, 587, 659, 698, 784, 880, 988, 1046]
notes_val = []
for note in notes:
   notes_val.append(beeper.calc_pitch(note))
#the length the shortest note and the pause
note_len = 0.1
pause_len = 0.1

Für den Aufruf der Sirene definiere ich eine Funktion

def tutatatu():
   global buzzTime
   buzzTime = time.ticks_ms()    
   beeper.play_value(note_len*4, pause_len, notes_val[8])
   beeper.play_value(note_len*2, pause_len, notes_val[5])
   beeper.play_value(note_len*2, pause_len, notes_val[5])
   beeper.play_value(note_len*4, pause_len, notes_val[8])

Das Einschalten der Sirene und der Wiederholungen nach 10 Sekunden erfolgt im Hauptteil des Programms

if cb & 2 == 2: 
   print("tutatatu")
   ticksNow = time.ticks_ms()
   print("ticksNow = ", ticksNow)
   print("buzzTime = ", buzzTime)
   if time.ticks_diff(ticksNow,buzzTime) > 10000:
       tutatatu()
else:
   pass

Da das Modul PIOBeep die StateMachine 0 benutzt, muss ich nun noch die StateMachine für die blaue LED ändern in

sm1 = rp2.StateMachine(1, blink_1hz, freq=2000, set_base=Pin(14))

Hier das komplette Programm für mein Robot Car mit Blaulicht und Sirene zum Download.

Viel Spaß beim Ausprobieren oder Anpassen an eigene Projekte.

Projekte für anfängerRaspberry pi

Lascia un commento

Tutti i commenti vengono moderati prima della pubblicazione