The Teseo-LIV3 GPS module (as used in shabaz ' GPS / Galileo / BeiDou / GLONASS receiver) talks UART and I2C. I'm writing an OO driver for it, for embedded systems.
goals:
- Teseo lib code does not need to know what the target microcontroller is.
- Teseo lib code does not need to know if the project uses I2C or UART
- controller and protocol functionality is provided by the user's project code. It has to plug in a reader and writer function.
In this first blog, I created a project that runs on a Pico, and uses I2C. The Teseo code doesn't know that. In this initial version, it's able to get location info ( $GPGLL message). That's the scenario documented in ST's application note AN5203 Teseo-LIV3F - I2C Positioning Sensor. Every second I ask the GPS for the location, and it 'll return the raw $GPGLL NMEA string. That's enough for a first version.
Pico resources:
- I2C0
- SDA: GP16
- SCL: GP17
- baud: 100 * 1000
Connections:
- 5V to VBUS
- 0V to GND
- SDA to GP16
- SCL to GP17
Software
The library has 1 class, teseo. It understands (a little part of) the Teseo command set and replies. For the communication, it relies on I2C functions that I provide. I'm reusing the Callback mechanism that I posted here.
The picture shows how the design uses dependency injection. The Testeo class can be used as-is on each microcontroller family. You give it (inject) the platform logic to read, write and reset.
I talk about reset here, but that's a subject for the next post, where I add that functionality.
teseo.h
#include <string> #include "callbackmanager.h" namespace teseo { class teseo { public: Callback<void, const std::string&>& getWriteCallback() { return writer; } Callback<void, std::string&>& getReadCallback() { return reader; } void write(const std::string& s); void read(std::string& s); void ask_gpgll() { write(gpgll_msg); } private: static const std::string gpgll_msg; Callback<void, const std::string&> writer; Callback<void, std::string&> reader; }; } // namespace teseo
The interface is simple. It allows you to set the callback for the reader and writer function, and provides a low level read and write. The first "smart" function, ask_gpgll(), can report your current location.
teseo.c
#include "teseo.h" namespace teseo { const std::string teseo::gpgll_msg = std::string("$PSTMNMEAREQUEST,100000,0\n\r"); void teseo::write(const std::string& s) { writer.call(s); } void teseo::read(std::string& s) { reader.call(s); } } // namespace teseo
Because I delegate the device dependent (and protocol dependent) communication mechanism, the Teseo class itself can focus on the Teseo's protocol. Its only role is to keep the conversation going. It will grow in a next version, when I'll learn it to parse the $GPGLL answer, and maybe do some other things like returning time.
If you look back at this class, you'll see that there are 0 lines that rely on Pico's SDK or on I2C.
main.cpp
Here is where we use the library, and we build the hardware dependent logic. All Pico and I2C code is in the main file. First the I2C initialisation:
#include <string> #include "teseo.h" #include "hardware/gpio.h" #include "hardware/i2c.h" #define I2C_PORT (i2c0) #define I2C_BAUD (100 * 1000) #define I2C_SDA (16) #define I2C_SCL (17) // #define I2C_ADDR (0x3A << 1) #define I2C_ADDR (0x3A) // ... void initcomms () { // I2C is "open drain", pull ups to keep signal high when no data is being sent (not. board has pullups) i2c_init(I2C_PORT, I2C_BAUD); gpio_set_function(I2C_SDA, GPIO_FUNC_I2C); gpio_set_function(I2C_SCL, GPIO_FUNC_I2C); // gpio_pull_up(I2C_SDA); // gpio_pull_up(I2C_SCL); }
Then the two callbacks for reading and writing:
void write(const std::string& s) { i2c_write_blocking(i2c_default, I2C_ADDR, reinterpret_cast<const uint8_t*>(s.c_str()), s.length() +1, false); return; } void read(std::string& s) { uint8_t buf[180] = { 0 }; // read in one go as register addresses auto-increment i2c_read_blocking(i2c_default, I2C_ADDR, buf, 180, false); // find first non 0xFF. That's the start auto iter_begin = std::find(std::begin(buf), std::end(buf), '$'); // find first 0xFF. That's the end auto iter_end = std::find(iter_begin, std::end(buf), 0xff); s = std::string(iter_begin, iter_end); return; }
There is some filtering needed. The reply sits "somewhere in the 180 character buffer". All other characters are 0xff. There are many ways to capture only the relevant data. Because I'm OO-designing, I decided to use one of the standard template library solutions.
in main(), I first setup the peripheral, and register the two callbacks:
teseo::teseo gps; // ... int main() { initcomms(); gps.getWriteCallback().set([](const std::string& s) -> void { write(s); }); gps.getReadCallback().set([](std::string& s) -> void { read(s); });
Then a loop that asks the location every second.
while (true) { std::string s(); gps.ask_gpgll(); gps.read(s); sleep_ms(1000); } }
The screen capture below shows that std::string s has the reply:
"$GPGLL,5051.80611,N,00422.57858,E,200736.000,V,N*4D\r\n"
Configure the Teseo as a position sensor: warning: this changes the default behaviour of the Teseo-LIV3F IC. By default, the Teseo sends a constant stream of various data via I2C. comparable to the output you see in @shabaz' blog. In this case, we want it to return only the data we're asking for. The ST application note shows the 3 commands you have execute to get this behaviour. You have to do it once before using the GPS (over UART, I2C or with Teseo-Suite). Alternatively, I could learn my lib to do that, but I prefer (at this moment) not to change the Teseo-LIV3F settings when running my example. To avoid getting angry comments in the blog. $PSTMCFGMSGL,3,1,0,0 (from the appnote:) This set of commands will:
Now the Teseo-LIV3F is configured to support I2C Positioning Sensor. If you want to get the default behaviour back, submit the command $PSTMRESTOREPAR. |
The code uses recent C++ functions. We tell that to the compiler in the CMake configuration file:
cmake_minimum_required(VERSION 3.13) set(PICO_DEOPTIMIZED_DEBUG 1) # Pull in SDK (must be before project) include(pico_sdk_import.cmake) # set(PICO_BOARD seeed_xiao_rp2040) project(pico_gps_teseo_i2c C CXX ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 20) pico_sdk_init() add_executable(${CMAKE_PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/teseo/teseo.cpp ) target_link_libraries(${CMAKE_PROJECT_NAME} pico_stdlib hardware_gpio hardware_i2c ) target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/teseo ${CMAKE_CURRENT_SOURCE_DIR}/callbackmanager ) pico_enable_stdio_uart(${CMAKE_PROJECT_NAME} 1) pico_enable_stdio_usb(${CMAKE_PROJECT_NAME} 0) pico_add_extra_outputs(${CMAKE_PROJECT_NAME} )
For reference:
Software manual
Application Note for I2C
Firmware details with commands list
This first version of the project is attached. The build directory contains the firmware .uf2 file. Thanks for reading.
pico_gps_teseo_i2c_20240713_02.zip
Next: C++ library for ST Teseo GPS - pt. 2: Dynamic GPS configuration (and some other things)
visit the github repository (git clone https://github.com/jancumps/pico_gps_teseo.git --recursive)
view the online documentation
Link to all posts.