This article is related to Fun With Arduino, Global Navigation Satellite Systems (GNSS) and Teseo III - again.
In the previous two posts, I created a device-and-comms-protocol independent C++ GPS library. The test code was for a Raspberry Pico and I2C though. It didn't test that independence.
The exercise proves that it's protocol independent. The same teseo class works with I2C and UART. As long as you tell it how to talk using that protocol.
Pico resources:
- UART1
- TX: GP4
- RX: GP5
- baud: 9600
- UART1 RX interrupt
- GPS RESET: GP18
Connections:
- 5V to VBUS
- 0V to GND
- RX to GP4
- TX to GP5
- RST to GP18
Software
As a firmware developer, you provide a reader and writer, just like in the previous i2c example. They should be able to send and get data from a UART peripheral. In this post, I used a Pico, its UART1, and the SDK UART API.
// ... #include "hardware/uart.h" #include <cassert> #define UART_PORT (uart1) #define UART_BAUD (9600) #define UART_TX (4) #define UART_RX (5) #define BUFFSIZE (180) #define UART_WAITFORREPLY_MS (200) // ... // forward declaration void on_uart_rx(); int UART_IRQ = UART1_IRQ; uint8_t *pBuf; // explicitely uninitialised volatile bool bWantChars; // explicitely uninitialised void init () { stdio_init_all(); // ... uart_init(UART_PORT, UART_BAUD); uart_set_fifo_enabled(UART_PORT, false); gpio_set_function(UART_TX, GPIO_FUNC_UART); gpio_set_function(UART_RX, GPIO_FUNC_UART); // set up and enable the interrupt handlers irq_set_exclusive_handler(UART_IRQ, on_uart_rx); irq_set_enabled(UART_IRQ, true); // ... } void on_uart_rx() { uint8_t letter; static uint8_t previousletter; if(pBuf == buf) { //* initialise previousletter at each buffer start previousletter = 0; } while (uart_is_readable(UART_PORT)) { letter = uart_getc(UART_PORT); if (bWantChars) { pBuf[0] = letter; if (pBuf[0] == '\n') { if (previousletter == '\n') { // two newlines is end of conversation bWantChars = false; } } if (pBuf[0] == 0) { bWantChars = false; // a null read } previousletter = letter; if ((pBuf - buf) < BUFFSIZE-1) { // if we reach max buffer size, just keep emptying any additional characters in the last position; pBuf++; } assert ((pBuf - buf) < BUFFSIZE); } } } void write(const std::string& s) { uart_write_blocking(UART_PORT, reinterpret_cast<const uint8_t*>(s.c_str()), s.length() +1); return; } void read(std::string& s) { memset (buf, 0, BUFFSIZE); // initialise buffer before reading pBuf = buf; bWantChars = true; // enable the UART to send interrupts - RX only uart_set_irq_enables(UART_PORT, true, false); absolute_time_t fail_at = delayed_by_ms(get_absolute_time(), UART_WAITFORREPLY_MS); while (bWantChars){ if (absolute_time_diff_us(fail_at, get_absolute_time()) >= 0) { bWantChars = false; // timeout } }; // disable the UART to send interrupts uart_set_irq_enables(UART_PORT, false, false); s = std::string(reinterpret_cast<const char*>(buf)); return; }
Did I have to change the original teseo class? Yes: one line.
In the original code, I only cleared the I2C buffer after reset. Because I was happily listening to the full UART data in parallel, to check GPS health.
That was just for develop purposes. Now that the code is OK for UART integration, I have to tame that UART output stream, just like I did with the I2C stream in previous posts.
One line is added in the init code of the teseo class:
// reset the UART message list write("$PSTMCFGMSGL,0,1,0,0\n\r"); // reset the I2C message list write("$PSTMCFGMSGL,3,1,0,0\n\r");
It does not change the behaviour of the I2C integration.
The current state of the interface, is that the teseo class can manage both communication protocols. And it behaves the same for both.
When you ask for data, it returns the same data. With the same signature. And with the same validations.
You 'll see that the read() functions aren't that simple. That's the case for both I2C and UART. This device isn't easy to control, and the read functions reflect that.
When porting to another controller, you 'll have similar challenges as I have with the Pico. Data doesn't "just arrive right". But I hope that my Pico example code helps to port to oother controllers and environments.
Anything else?
yes:
- you can choose between UART and I2C in the makefile
# select the communication protocol to talk to the Teseo GPS #add_compile_definitions(GPS_OVER_UART) add_compile_definitions(GPS_OVER_I2C)
This doesn't influence the teseo class. It tells the example code to use I2C or UART.
- I introduce a new C++STL concept: a pair.
A pair can hold two things. The name actually reveals the intent. In the latest code, I use it to hold NMEA commands, and the validation string to check if the reply is valid.
typedef const std::pair<const std::string, const std::string> nmea_rr; // ... private: //! command to retrieve GPGLL data static nmea_rr gpgll; //! command to retrieve GPRMC data static nmea_rr gprmc; // ... nmea_rr teseo::gpgll("$PSTMNMEAREQUEST,100000,0\n\r", "$GPGLL,"); nmea_rr teseo::gprmc("$PSTMNMEAREQUEST,40,0\n\r", "$GPRMC,"); bool teseo::ask_nmea(const nmea_rr& command, std::string& s, uint retries) { bool retval; // intentionally not initialised uint tries; // intentionally not initialised for (tries = 0; tries <= retries; tries++){ write(command.first); read(s); retval = s.starts_with(command.second); if (retval) { break; } } return retval; } // ...
It makes the class interface smaller (1 member for each command, in stead of 2). Without introducing a new class or structure. I use an existing stl feature.
It also allows me to create a generic reply-response handler, that always applies the right return value validation for the right command.
Discuss below.
Next post: C++ library for ST Teseo GPS - pt. 4: PCB to Pico hardware connections
visit the github repository (git clone https://github.com/jancumps/pico_gps_teseo.git --recursive)
view the online documentation
Link to all posts.