You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Integration of Pi Pico into the Donkeycar pin ecosystem
Because PiGPIO is not supported on RPi 5 any longer we have to move the pin support for realtime pin tasks to the Pi Pico. This allows to migrate PWM input pins onto the Pico for using an RC controller to drive the car, as well as using the Pico's PWM pins to provide output pins for the servo and ESC. Other pins that required realtime signals like odometery or IR receivers can also be migrated to the Pico because we also support a PulseIn pin. In addition we aim to add support for the 4 analog pins to the pico, which for example could be used to monitor the battery voltage.
The following new class will be added. This is not a DonkeyCar part, but a low-level serial communication link between the above mentioned pins and the Pico board, connected to the RPi. It will send serial data as fast as possible back and forth between the Pico and the internal pin cache of the car, such that signals can be transferred at much higher rates than the car frequency. This class runs the serial connection in its own thread.
classPico:
""" Pi Pico class to communicate with the Pico over usb. We use the same usb cable for power and data. The Pico is connected to the computer and can handle file copy / repl on /dev/ttyACM0 while doing bidirectional data transfer on /dev/ttyACM1. To set up the pins on the pico, we need to send a configuration dictionary. An example is here: pin_configuration = { 'input_pins': { 'GP13': dict(mode='INPUT', pull_up=False), 'GP18': dict(mode='PULSE_IN', maxlen=8, auto_clear=True), 'GP16': dict(mode='PULSE_IN', maxlen=4), 'GP17': dict(mode='PWM_IN', duty=0.09), 'GP28': dict(mode='ANALOG_IN'), }, 'output_pins': { 'GP14': dict(mode='OUTPUT', value=0), 'GP15': dict(mode='PWM', frequency=60, duty_cycle=0.06), }, } The dictionary is required to have either 'input_pins' or 'output_pins' or both as keys. Input and output refers to the pins on the pico. The values of 'input_pins' and 'output_pins' contain dictionaries with the pin name as key and the pin configuration as value. The pin configuration is a dictionary with key-values depending on the mode of the pin. The 'mode' key is required for all pins. The different supported input modes are: 'INPUT': for digital input 'PULSE_IN': for pulse input 'PWM_IN': for pwm input from an RC controller 'ANALOG_IN': for analog input The different output modes are: 'OUTPUT': for digital output 'PWM': for pulse width modulation output See above examples for the required keys for each mode. """def__init__(self, port: str='/dev/ttyACM1'):
""" Initialize the Pico communicator. :param port: port for data connection """self.serial=serial.Serial(port, 115200)
self.counter=0self.running=Trueself.pin_configuration=dict()
self.send_dict=dict()
self.receive_dict=dict()
self.lock=Lock()
self.start=Nonelogger.info(f"Pico created on port: {port}")
# send the initial setup dictionary to clear all pinspack=json.dumps(dict(input_pins={}, output_pins={})) +'\n'self.serial.write(pack.encode())
self.t=Thread(target=self.loop, args=(), daemon=True)
self.t.start()
defloop(self):
""" Donkey parts interface. We are sending newline delimited json strings and expect the same in return. """# clear the input bufferself.serial.reset_input_buffer()
self.start=time.time()
# start loop of continuous communicationwhileself.running:
try:
pack=Nonewithself.lock:
pack=json.dumps(self.send_dict) +'\n'self.serial.write(pack.encode())
time.sleep(0)
bytes_in=self.serial.read_until()
time.sleep(0)
str_in=bytes_in.decode()[:-1]
received_dict=json.loads(str_in)
withself.lock:
self.receive_dict.update(received_dict)
ifself.counter%1000==0:
logger.debug(f'Last sent: {pack}')
logger.debug(f'Last received: {str_in}')
exceptValueErrorase:
logger.error(f'Failed to load json in loop {self.counter} 'f'because of {e}. Expected json, but got: 'f'+++{str_in}+++')
exceptExceptionase:
logger.error(f'Problem with serial input {e}')
self.counter+=1logger.info('Pico loop stopped.')
defwrite(self, gpio: str, value: floatorint) ->None:
""" :param gpio: the gpio pin to write to :param value: the value to write """# Wait until threaded loop has at least run once, so we don't have to)# process None values. This blocks until the first data is received.whileself.counter==0:
time.sleep(0.1)
assertgpioinself.send_dict, f"Pin {gpio} not in send_dict."withself.lock:
self.send_dict[gpio] =valuedefread(self, gpio):
""" :param gpio: the gpio pin to read from :return: the value of the pin """# Wait until threaded loop has at least run once, so we don't have to# process None values. This blocks until the first data is received.whileself.counter==0:
time.sleep(0.1)
withself.lock:
ifgpionotinself.receive_dict:
msg= (f"Pin {gpio} not in receive_dict. Known pins: "f"{', '.join(self.receive_dict.keys())}")
logger.error(msg)
raiseRuntimeError(msg)
returnself.receive_dict[gpio]
defstop(self):
logger.info("Stopping Pico communication.")
self.running=Falsetime.sleep(0.1)
self.t.join()
logger.info("Pico communication stopped.")
self.serial.close()
total_time=time.time() -self.startlogger.info(f"Pico communication disconnected, ran {self.counter} "f"loops, each loop taking "f"{total_time*1000/self.counter:5.1f} ms.")
defsetup_input_pin(self, gpio: str, mode: str, **kwargs) ->None:
""" :param gpio: the gpio pin to set up :param mode: the mode of the pin :param kwargs: additional arguments for the mode """assertmodein ('INPUT', 'PULSE_IN', 'ANALOG_IN', 'PWM_IN'), \
f"Mode {mode} not supported for input pins."setup_dict=dict(input_pins={gpio: dict(mode=mode, **kwargs)})
logger.info(f"Setting up input pin {gpio} in mode {mode} using "f"setup dict {setup_dict}")
withself.lock:
# send the setup dictionarypack=json.dumps(setup_dict) +'\n'self.serial.write(pack.encode())
self.receive_dict[gpio] =0defsetup_output_pin(self, gpio: str, mode: str, **kwargs) ->None:
""" :param gpio: the gpio pin to set up :param mode: the mode of the pin :param kwargs: additional arguments for the mode """assertmodein ('OUTPUT', 'PWM'), \
f"Mode {mode} not supported for output pins on Pico"setup_dict=dict(output_pins={gpio: dict(mode=mode, **kwargs)})
logger.info(f"Setting up output pin {gpio} in mode {mode} using "f"setup dict {setup_dict}")
withself.lock:
# send the setup dictionarypack=json.dumps(setup_dict) +'\n'self.serial.write(pack.encode())
self.send_dict[gpio] =0ifmode=='OUTPUT'elsekwargs['duty']
defremove_pin(self, gpio: str) ->None:
""" :param gpio: the gpio pin to remove """setup_dict=dict()
logger.info(f"Removing pin {gpio}")
ifgpioinself.receive_dict:
setup_dict['input_pins'] = {gpio: {}}
delself.receive_dict[gpio]
elifgpioinself.send_dict:
setup_dict['output_pins'] = {gpio: {}}
delself.send_dict[gpio]
else:
logger.warning(f"Pin {gpio} not in send or receive dict.")
returnwithself.lock:
# send the setup dictionarypack=json.dumps(setup_dict) +'\n'self.serial.write(pack.encode())
Circuitpython code and usage for the Pico
Circuitpython code to be installed on the Pico by copying files. Note, this has to be done only once. All pin creations and deletions will then be handled in software only. The boot.py file enables the second usb channel for communication. That channel will usually show up on the Pi under /dev/ttyACM1. The shell for accessing the REPL or the print command outputs can be accessed under /dev/ttyACM0.
As you can see, this was a simple session where only a single PWM pin was created and deleted. Deletion happens by calling stop() on the pin. Here is the output of another session where multiple pins were created and deleted. This is the output created by running the pins.py script multiple times with different arguments creating different pins.
The output above was created by initialising and running two digital input and output pins in the python interpreter (GP0 and GP1) and afterwards calling the pin.py script with different parameters like:
pins.py -w PICO.BCM.19 -tm 10
Viewing serial output for debugging
The easiest way to view the above output is by installing and running tio, a linux terminal program that can be attached to the serial port by tio /dev/ttyACM0. Note, that tio provides you with a terminal, which means you can type Ctrl-C to enter the REPL and work on the Pico interactively or press Ctrl-D to return to the above program. The corresponding familiar terminal code shown on the end of that last session.
The text was updated successfully, but these errors were encountered:
Integration of Pi Pico into the Donkeycar pin ecosystem
Because PiGPIO is not supported on RPi 5 any longer we have to move the pin support for realtime pin tasks to the Pi Pico. This allows to migrate PWM input pins onto the Pico for using an RC controller to drive the car, as well as using the Pico's PWM pins to provide output pins for the servo and ESC. Other pins that required realtime signals like odometery or IR receivers can also be migrated to the Pico because we also support a PulseIn pin. In addition we aim to add support for the 4 analog pins to the pico, which for example could be used to monitor the battery voltage.
Changes in the
pins
moduleThe following new pins will be added:
The pico serial communicator
The following new class will be added. This is not a DonkeyCar part, but a low-level serial communication link between the above mentioned pins and the Pico board, connected to the RPi. It will send serial data as fast as possible back and forth between the Pico and the internal pin cache of the car, such that signals can be transferred at much higher rates than the car frequency. This class runs the serial connection in its own thread.
Circuitpython code and usage for the Pico
Circuitpython code to be installed on the Pico by copying files. Note, this has to be done only once. All pin creations and deletions will then be handled in software only. The
boot.py
file enables the second usb channel for communication. That channel will usually show up on the Pi under/dev/ttyACM1
. The shell for accessing the REPL or the print command outputs can be accessed under/dev/ttyACM0
.Running on the Pico
Here is a shell output from running above code on the Pico:
As you can see, this was a simple session where only a single PWM pin was created and deleted. Deletion happens by calling
stop()
on the pin. Here is the output of another session where multiple pins were created and deleted. This is the output created by running thepins.py
script multiple times with different arguments creating different pins.The output above was created by initialising and running two digital input and output pins in the python interpreter (GP0 and GP1) and afterwards calling the
pin.py
script with different parameters like:Viewing serial output for debugging
The easiest way to view the above output is by installing and running
tio
, a linux terminal program that can be attached to the serial port bytio /dev/ttyACM0
. Note, thattio
provides you with a terminal, which means you can typeCtrl-C
to enter the REPL and work on the Pico interactively or pressCtrl-D
to return to the above program. The corresponding familiar terminal code shown on the end of that last session.The text was updated successfully, but these errors were encountered: