Hi, I’m looking for a Micropython driver for a USB to UART converter (CP2102). The converter is for a Pico project (off-grid power system - logging Victron ‘smart shunt’ data to SD). I’ve found some code written in C but am not familiar enough with C and drivers.
This quickly became a deep dive. Victron has a few resources on their different products can output data and how to configure them to do so. The smart shunt you are limited to interface (depending on the shunt).
Could you please share what you found in C? That will hopefully give us a spot to start.
Hi Jack,
Thanks for the quick reply. Yes, Victron has published how the shunt data is formatted and accessed. I’ve attempted reading via the Victron-recommended VE.direct cable and the Pico’s USB C port. The cable should be providing 3.3V of power but it won’t power up the Pico. I tried the same cable with a solar controller which should be providing 5V and it also wouldn’t power the Pico. Hence I’m trying the USB-UART approach.
The C-code is included below:
The Core description for CP2102 says its an “easy-to-use USB to UART converter. Featuring the highly-regarded CP2102 chip and plug-and-play functionality, this converter provides a stable and fast connection with minimal setup.”
// SPDX-License-Identifier: MIT
/*
* Copyright 2021 Álvaro Fernández Rojas <noltari@gmail.com>
*/
#include <hardware/irq.h>
#include <hardware/structs/sio.h>
#include <hardware/uart.h>
#include <pico/multicore.h>
#include <pico/stdlib.h>
#include <string.h>
#include <tusb.h>
#if !defined(MIN)
#define MIN(a, b) ((a > b) ? b : a)
#endif /* MIN */
#define LED_PIN 25
#define BUFFER_SIZE 2560
#define DEF_BIT_RATE 115200
#define DEF_STOP_BITS 1
#define DEF_PARITY 0
#define DEF_DATA_BITS 8
typedef struct {
uart_inst_t *const inst;
uint irq;
void *irq_fn;
uint8_t tx_pin;
uint8_t rx_pin;
} uart_id_t;
typedef struct {
cdc_line_coding_t usb_lc;
cdc_line_coding_t uart_lc;
mutex_t lc_mtx;
uint8_t uart_buffer[BUFFER_SIZE];
uint32_t uart_pos;
mutex_t uart_mtx;
uint8_t usb_buffer[BUFFER_SIZE];
uint32_t usb_pos;
mutex_t usb_mtx;
} uart_data_t;
void uart0_irq_fn(void);
void uart1_irq_fn(void);
const uart_id_t UART_ID[CFG_TUD_CDC] = {
{
.inst = uart0,
.irq = UART0_IRQ,
.irq_fn = &uart0_irq_fn,
.tx_pin = 16,
.rx_pin = 17,
}, {
.inst = uart1,
.irq = UART1_IRQ,
.irq_fn = &uart1_irq_fn,
.tx_pin = 4,
.rx_pin = 5,
}
};
uart_data_t UART_DATA[CFG_TUD_CDC];
static inline uint databits_usb2uart(uint8_t data_bits)
{
switch (data_bits) {
case 5:
return 5;
case 6:
return 6;
case 7:
return 7;
default:
return 8;
}
}
static inline uart_parity_t parity_usb2uart(uint8_t usb_parity)
{
switch (usb_parity) {
case 1:
return UART_PARITY_ODD;
case 2:
return UART_PARITY_EVEN;
default:
return UART_PARITY_NONE;
}
}
static inline uint stopbits_usb2uart(uint8_t stop_bits)
{
switch (stop_bits) {
case 2:
return 2;
default:
return 1;
}
}
void update_uart_cfg(uint8_t itf)
{
const uart_id_t *ui = &UART_ID[itf];
uart_data_t *ud = &UART_DATA[itf];
mutex_enter_blocking(&ud->lc_mtx);
if (ud->usb_lc.bit_rate != ud->uart_lc.bit_rate) {
uart_set_baudrate(ui->inst, ud->usb_lc.bit_rate);
ud->uart_lc.bit_rate = ud->usb_lc.bit_rate;
}
if ((ud->usb_lc.stop_bits != ud->uart_lc.stop_bits) ||
(ud->usb_lc.parity != ud->uart_lc.parity) ||
(ud->usb_lc.data_bits != ud->uart_lc.data_bits)) {
uart_set_format(ui->inst,
databits_usb2uart(ud->usb_lc.data_bits),
stopbits_usb2uart(ud->usb_lc.stop_bits),
parity_usb2uart(ud->usb_lc.parity));
ud->uart_lc.data_bits = ud->usb_lc.data_bits;
ud->uart_lc.parity = ud->usb_lc.parity;
ud->uart_lc.stop_bits = ud->usb_lc.stop_bits;
}
mutex_exit(&ud->lc_mtx);
}
void usb_read_bytes(uint8_t itf)
{
uart_data_t *ud = &UART_DATA[itf];
uint32_t len = tud_cdc_n_available(itf);
if (len &&
mutex_try_enter(&ud->usb_mtx, NULL)) {
len = MIN(len, BUFFER_SIZE - ud->usb_pos);
if (len) {
uint32_t count;
count = tud_cdc_n_read(itf, &ud->usb_buffer[ud->usb_pos], len);
ud->usb_pos += count;
}
mutex_exit(&ud->usb_mtx);
}
}
void usb_write_bytes(uint8_t itf)
{
uart_data_t *ud = &UART_DATA[itf];
if (ud->uart_pos &&
mutex_try_enter(&ud->uart_mtx, NULL)) {
uint32_t count;
count = tud_cdc_n_write(itf, ud->uart_buffer, ud->uart_pos);
if (count < ud->uart_pos)
memmove(ud->uart_buffer, &ud->uart_buffer[count],
ud->uart_pos - count);
ud->uart_pos -= count;
mutex_exit(&ud->uart_mtx);
if (count)
tud_cdc_n_write_flush(itf);
}
}
void usb_cdc_process(uint8_t itf)
{
uart_data_t *ud = &UART_DATA[itf];
mutex_enter_blocking(&ud->lc_mtx);
tud_cdc_n_get_line_coding(itf, &ud->usb_lc);
mutex_exit(&ud->lc_mtx);
usb_read_bytes(itf);
usb_write_bytes(itf);
}
void core1_entry(void)
{
tusb_init();
while (1) {
int itf;
int con = 0;
tud_task();
for (itf = 0; itf < CFG_TUD_CDC; itf++) {
if (tud_cdc_n_connected(itf)) {
con = 1;
usb_cdc_process(itf);
}
}
gpio_put(LED_PIN, con);
}
}
static inline void uart_read_bytes(uint8_t itf)
{
uart_data_t *ud = &UART_DATA[itf];
const uart_id_t *ui = &UART_ID[itf];
if (uart_is_readable(ui->inst)) {
mutex_enter_blocking(&ud->uart_mtx);
while (uart_is_readable(ui->inst) &&
(ud->uart_pos < BUFFER_SIZE)) {
ud->uart_buffer[ud->uart_pos] = uart_getc(ui->inst);
ud->uart_pos++;
}
mutex_exit(&ud->uart_mtx);
}
}
void uart0_irq_fn(void)
{
uart_read_bytes(0);
}
void uart1_irq_fn(void)
{
uart_read_bytes(1);
}
void uart_write_bytes(uint8_t itf)
{
uart_data_t *ud = &UART_DATA[itf];
if (ud->usb_pos &&
mutex_try_enter(&ud->usb_mtx, NULL)) {
const uart_id_t *ui = &UART_ID[itf];
uint32_t count = 0;
while (uart_is_writable(ui->inst) &&
count < ud->usb_pos) {
uart_putc_raw(ui->inst, ud->usb_buffer[count]);
count++;
}
if (count < ud->usb_pos)
memmove(ud->usb_buffer, &ud->usb_buffer[count],
ud->usb_pos - count);
ud->usb_pos -= count;
mutex_exit(&ud->usb_mtx);
}
}
void init_uart_data(uint8_t itf)
{
const uart_id_t *ui = &UART_ID[itf];
uart_data_t *ud = &UART_DATA[itf];
/* Pinmux */
gpio_set_function(ui->tx_pin, GPIO_FUNC_UART);
gpio_set_function(ui->rx_pin, GPIO_FUNC_UART);
/* USB CDC LC */
ud->usb_lc.bit_rate = DEF_BIT_RATE;
ud->usb_lc.data_bits = DEF_DATA_BITS;
ud->usb_lc.parity = DEF_PARITY;
ud->usb_lc.stop_bits = DEF_STOP_BITS;
/* UART LC */
ud->uart_lc.bit_rate = DEF_BIT_RATE;
ud->uart_lc.data_bits = DEF_DATA_BITS;
ud->uart_lc.parity = DEF_PARITY;
ud->uart_lc.stop_bits = DEF_STOP_BITS;
/* Buffer */
ud->uart_pos = 0;
ud->usb_pos = 0;
/* Mutex */
mutex_init(&ud->lc_mtx);
mutex_init(&ud->uart_mtx);
mutex_init(&ud->usb_mtx);
/* UART start */
uart_init(ui->inst, ud->usb_lc.bit_rate);
uart_set_hw_flow(ui->inst, false, false);
uart_set_format(ui->inst, databits_usb2uart(ud->usb_lc.data_bits),
stopbits_usb2uart(ud->usb_lc.stop_bits),
parity_usb2uart(ud->usb_lc.parity));
uart_set_fifo_enabled(ui->inst, false);
/* UART RX Interrupt */
irq_set_exclusive_handler(ui->irq, ui->irq_fn);
irq_set_enabled(ui->irq, true);
uart_set_irq_enables(ui->inst, true, false);
}
int main(void)
{
int itf;
usbd_serial_init();
for (itf = 0; itf < CFG_TUD_CDC; itf++)
init_uart_data(itf);
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
multicore_launch_core1(core1_entry);
while (1) {
for (itf = 0; itf < CFG_TUD_CDC; itf++) {
update_uart_cfg(itf);
uart_write_bytes(itf);
}
}
return 0;
}
It might be prudent for me to do some expectation management for this one re: the CP2102
I think what you’re describing is not electronically possible.
If you connect this to your Pico then you can connect a USB lead to the CP2012 which will appear as a com port on your computer. From there you can send data between a computer and the Pico via the Pico’s UART interface.
The CP2102 is not intended to work as a USB Host as far as i can tell. I don’t think it will be possible to connect these up in the following way:
Pico UART pins ↔ CP2102 ↔ USB Cable ↔ Victron device (USB device)
Rather it’s intended to work as follows
Pico UART pins ↔ CP2102 ↔ USB Cable ↔ Computer (USB Host)
In summary, just because the unit has a USB port on it, and just because the Pico can be adapted to USB Serial using a CP2102, doesn’t mean they will communicate.
All is not lost though - it looks like the the smart shunt has a CAN interface, which might get you over the line
Here’s one from adafruit
[ ADA5728 ] Adafruit PiCowbell CAN Bus for Pico - MCP2515 CAN Controller
And another from Waveshare. This one has minimal Python example code to get you started.
[ CE09488 ] CAN bus Module (B) for Raspberry Pi Pico
Thanks Michael,
I’ve learned a lot from your reply and it’s clear that the USB-UART approach won’t work. I can see various posts about using the Pico as a USB Host - so will explore that next.
Last resort is Rpi but the Pico’s low power consumption is much better for this application (off-grid house, small power system).
Don’t discount the CAN bus first @John265780 - if you can find suitable driver code for the Pico it could be a real win, and is exactly intended for what you want to do: low-power comms to a host microcontroller.
Here’s a nice, free resource from the Uni of Newcastle that can help fill in the gaps: CAN Bus Zero 2 Hero
Thanks Michael,
The Victron Smart Shunt only has a VE.Direct port, so I’m limited to the USB cable connection. Bluetooth is available but I prefer simpler solutions if there are any and Bluetooth range is really poor. I’ve now got a power supply (from Core electronics) for a breadboard and can power the Pico with that. The Smart Shunt is supposed to be sending out lines of serial data every minute via the VE.Direct port, so I’ll try to read it via the Victron USB cable. The Shunt has a power pin (supposedly 3.3V) but it wouldn’t power the Pico when I tested it. Somehow, I’d like to prevent the Pico from accepting any power via the USB connector.
Hey John,
The Pico is configured to accept power for the highest voltage source. For example if you are providing 3.3v to the VSYS and GND pins on the board and 5V over USB, it will accept the power over USB. if you could provide the Pico with more than 5V power through the pins it would switch to using that power source.
From some light research it doesn’t seem like there is a way to disable power over USB on the pi through software. You may need to look into doing this by modifying your cable to data only although I’m not sure how the USB protocol handles that.
It’s odd that the 3.3V output from the Shunt is not powering the Pico. Anything in the range of 1.5 to 5.5V should be acceptable for the Pico so this may need for more investigation.
Hope this helps!
Sam