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.