I have a Raspberry Pico GPS library project. It comes with several examples. And each example can communicate to the GPS over I2C or UART.
Initially, it was a single-example project. I used a definition in the NMake config, that allowed you to build the project for either I2C or UART. Based on that parameter, it would generate a RP2040 .uf2 firmware that matched your choice.
I wasn't too happy with that approach, because a user was required to change the build file. And I had to run several builds when I was about to generate the assets for a new release.
Multiple Firmwares
If you analyse my challenge, it comes down to this:
- I have multiple examples that use my Teseo communication code
- The Teseo communication code can work on I2C or UART
I split the work up. First, I learned NMake to build two libraries for my communication code. One for I2C, and one for UART:
# sources shared by all examples with uart
add_library(${CMAKE_PROJECT_NAME}_shared_uart
${CMAKE_CURRENT_SOURCE_DIR}/gps_teseo_lib/teseo/teseo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/port/pico/reset.cpp
${CMAKE_CURRENT_SOURCE_DIR}/port/pico/uart/teseo_communicate.cpp
)
target_include_directories(${CMAKE_PROJECT_NAME}_shared_uart PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/gps_teseo_lib/teseo
${CMAKE_CURRENT_SOURCE_DIR}/gps_teseo_lib/callbackmanager
${CMAKE_CURRENT_SOURCE_DIR}/port/pico
${CMAKE_CURRENT_SOURCE_DIR}/port/pico/uart
)
target_link_libraries(${CMAKE_PROJECT_NAME}_shared_uart PUBLIC
pico_stdlib hardware_gpio hardware_uart
)
# sources shared by all examples with i2c
add_library(${CMAKE_PROJECT_NAME}_shared_i2c
${CMAKE_CURRENT_SOURCE_DIR}/gps_teseo_lib/teseo/teseo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/port/pico/reset.cpp
${CMAKE_CURRENT_SOURCE_DIR}/port/pico/i2c/teseo_communicate.cpp
)
target_include_directories(${CMAKE_PROJECT_NAME}_shared_i2c PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/gps_teseo_lib/teseo
${CMAKE_CURRENT_SOURCE_DIR}/gps_teseo_lib/callbackmanager
${CMAKE_CURRENT_SOURCE_DIR}/port/pico
${CMAKE_CURRENT_SOURCE_DIR}/port/pico/i2c
)
target_link_libraries(${CMAKE_PROJECT_NAME}_shared_i2c
pico_stdlib hardware_gpio hardware_i2c
)
Then, for each of my examples, I generate one binary that links to the I2C version. And one that links to the UART lib. Because the communication part is fully abstracted, I don't need conditional compilation. The linker can resolve it all.
# teseo reply response example --------------------------------------
add_executable(${CMAKE_PROJECT_NAME}_reply_response_uart
${CMAKE_CURRENT_SOURCE_DIR}/teseo_reply_response.cpp
)
target_link_libraries(${CMAKE_PROJECT_NAME}_reply_response_uart
${CMAKE_PROJECT_NAME}_shared_uart
)
target_include_directories(${CMAKE_PROJECT_NAME}_reply_response_uart PUBLIC
)
# select the debug output (not used by the GPS interface)
pico_enable_stdio_uart(${CMAKE_PROJECT_NAME}_reply_response_uart 1)
pico_enable_stdio_usb(${CMAKE_PROJECT_NAME}_reply_response_uart 0)
pico_add_extra_outputs(${CMAKE_PROJECT_NAME}_reply_response_uart )
add_executable(${CMAKE_PROJECT_NAME}_reply_response_i2c
${CMAKE_CURRENT_SOURCE_DIR}/teseo_reply_response.cpp
)
target_link_libraries(${CMAKE_PROJECT_NAME}_reply_response_i2c
${CMAKE_PROJECT_NAME}_shared_i2c
)
target_include_directories(${CMAKE_PROJECT_NAME}_reply_response_i2c PUBLIC
)
# select the debug output (not used by the GPS interface)
pico_enable_stdio_uart(${CMAKE_PROJECT_NAME}_reply_response_i2c 1)
pico_enable_stdio_usb(${CMAKE_PROJECT_NAME}_reply_response_i2c 0)
pico_add_extra_outputs(${CMAKE_PROJECT_NAME}_reply_response_i2c )
# nmea parse example --------------------------------------
add_executable(${CMAKE_PROJECT_NAME}_nmea_parse_uart
${CMAKE_CURRENT_SOURCE_DIR}/teseo_with_nmea_parse.cpp
)
target_link_libraries(${CMAKE_PROJECT_NAME}_nmea_parse_uart
${CMAKE_PROJECT_NAME}_shared_uart
)
target_include_directories(${CMAKE_PROJECT_NAME}_nmea_parse_uart PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/gps_nmea_lib/nmea
)
# select the debug output (not used by the GPS interface)
pico_enable_stdio_uart(${CMAKE_PROJECT_NAME}_nmea_parse_uart 1)
pico_enable_stdio_usb(${CMAKE_PROJECT_NAME}_nmea_parse_uart 0)
pico_add_extra_outputs(${CMAKE_PROJECT_NAME}_nmea_parse_uart )
add_executable(${CMAKE_PROJECT_NAME}_nmea_parse_i2c
${CMAKE_CURRENT_SOURCE_DIR}/teseo_with_nmea_parse.cpp
)
target_link_libraries(${CMAKE_PROJECT_NAME}_nmea_parse_i2c
${CMAKE_PROJECT_NAME}_shared_i2c
)
target_include_directories(${CMAKE_PROJECT_NAME}_nmea_parse_i2c PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/gps_nmea_lib/nmea
)
# select the debug output (not used by the GPS interface)
pico_enable_stdio_uart(${CMAKE_PROJECT_NAME}_nmea_parse_i2c 1)
pico_enable_stdio_usb(${CMAKE_PROJECT_NAME}_nmea_parse_i2c 0)
pico_add_extra_outputs(${CMAKE_PROJECT_NAME}_nmea_parse_i2c )
That's it. If you run NMake without target, it 'll build all permutations. If you only want a single firmware, you can define that as a target during build.
VSCode, and likely all of the other IDEs, have an option for that. For CLI users: you can also pass your (or no) target on the command line.
Link to all posts.