Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make reboot.h compatible with C++ #11

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ add_executable(picowota
main.c
tcp_comm.c
dhcpserver/dhcpserver.c
display_minimal.c
uc1701_display_minimal.c
eeprom.c
cat24c256_eeprom.c
)

function(target_cl_options option)
Expand All @@ -48,13 +52,15 @@ target_cl_options("-Wall")
target_cl_options("-Os")
target_cl_options("-ffunction-sections")
target_cl_options("-fdata-sections")
target_link_options(picowota PRIVATE "LINKER:--gc-sections")
target_link_options(picowota PRIVATE "LINKER:--gc-sections,--print-memory-usage")

pico_add_extra_outputs(picowota)

target_include_directories(picowota PRIVATE
${CMAKE_CURRENT_LIST_DIR} # Needed so that lwip can find lwipopts.h
${CMAKE_CURRENT_LIST_DIR}/dhcpserver)
${CMAKE_CURRENT_LIST_DIR}/dhcpserver
${CMAKE_SOURCE_DIR}/targets
)

pico_enable_stdio_usb(picowota 1)

Expand All @@ -68,9 +74,12 @@ target_link_libraries(picowota
hardware_structs
pico_cyw43_arch_lwip_poll
pico_stdlib
hardware_spi
hardware_i2c
pico_sync
pico_util
picowota_reboot
u8g2
)

# Retrieves build variables from the environment if present
Expand Down
105 changes: 4 additions & 101 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,85 +2,13 @@

> `picowota`, kinda sounds like you're speaking [Belter](https://expanse.fandom.com/wiki/Belter)

This project implements a bootloader for the Raspberry Pi Pico W which allows
upload of program code over WiFi ("Over The Air").
This project implements a bootloader for the Raspberry Pi Pico W for the OpenTrickler Project

The easiest way to use it is to include this repository as a submodule in the
application which you want to be able to update over WiFi.
credits to: @usedbytes / https://github.com/usedbytes/picowota

There's an example project using picowota at https://github.com/usedbytes/picowota_blink
OpenTrickler: https://github.com/dirtbit/OpenTrickler-RP2040-Controller, developed by @earms:
https://github.com/eamars/OpenTrickler-RP2040-Controller

## Using in your project

First add `picowota` as a submodule to your project:
```
git submodule add https://github.com/usedbytes/picowota
git submodule update --init picowota
git commit -m "Add picowota submodule"
```

Then modifiy your project's CMakeLists.txt to include the `picowota` directory:

```
add_subdirectory(picowota)
```

`picowota` either connects to an existing WiFi network (by default) or
creates one, in both cases with the given SSID and password.

You can either provide the following as environment variables, or set them
as CMake variables:

```
PICOWOTA_WIFI_SSID # The WiFi network SSID
PICOWOTA_WIFI_PASS # The WiFi network password
PICOWOTA_WIFI_AP # Optional; 0 = connect to the network, 1 = create it
```

Then, you can either build just your standalone app binary (suitable for
updating via `picowota` when it's already on the Pico), or a combined binary
which contains the bootloader and the app (suitable for flashing the first
time):

```
picowota_build_standalone(my_executable_name)
picowota_build_combined(my_executable_name)
```

Note: The combined target will also build the standalone binary.

To be able to update your app, you must provide a way to return to the
bootloader. By default, if GPIO15 is pulled low at boot time, then `picowota`
will stay in bootloader mode, ready to receive new app code.

You can also return to the bootloader from your app code - for example when a
button is pressed, or in response to some network request. The
`picowota_reboot` library provides a `picowota_reboot(bool to_bootloader)`
function, which your app can call to get back in to the bootloader.

```
CMakeLists.txt:

target_link_libraries(my_executable_name picowota_reboot)

your_c_code.c:

#include "picowota/reboot.h"

...

{

...

if (should_reboot_to_bootloader) {
picowota_reboot(true);
}

...

}
```

## Uploading code via `picowota`

Expand All @@ -102,28 +30,3 @@ serial-flash tcp:192.168.1.123:4242 my_executable_name.elf

After uploading the code, if successful, the Pico will jump to the newly
uploaded app.

## How it works

This is derived from my Pico non-W bootloader, https://github.com/usedbytes/rp2040-serial-bootloader, which I wrote about in a blog post: https://blog.usedbytes.com/2021/12/pico-serial-bootloader/

The bootloader code attempts to avoid "bricking" by storing a CRC of the app
code which gets uploaded. If the CRC doesn't match, then the app won't get run
and the Pico will stay in `picowota` bootloader mode. This should make it fairly
robust against errors in transfers etc.

## Known issues

### Bootloader/app size and `cyw43` firmware

The WiFi chip on a Pico W needs firmware, which gets built in to any program
you build with the Pico SDK. This is relatively large - 300-400 kB, which is why
this bootloader is so large.

This gets duplicated in the `picowota` bootloader binary and _also_ the app
binary, which obviously uses up a significant chunk of the Pico's 2 MB flash.

It would be nice to be able to avoid this duplication, but the Pico SDK
libraries don't give a mechanism to do so.

I've raised https://github.com/raspberrypi/pico-sdk/issues/928 for consideration.
99 changes: 99 additions & 0 deletions cat24c256_eeprom.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include "hardware/spi.h"
#include "hardware/uart.h"

#ifdef RASPBERRYPI_PICO
#include "raspberrypi_pico_config.h"
#endif

#ifdef RASPBERRYPI_PICO_W
#include "raspberrypi_pico_w_config.h"
#endif
#include "eeprom.h"

// Include only for PICO board with specific flash chip
#include "pico/unique_id.h"


#define PAGE_SIZE 64 // 64 byte page size


void cat24c256_eeprom_init() {
// Initialize I2C bus with 400k baud rate
i2c_init(EEPROM_I2C, 400 * 1000);

// Initialize PINs as I2C function
gpio_set_function(EEPROM_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(EEPROM_SCL_PIN, GPIO_FUNC_I2C);

gpio_pull_up(EEPROM_SDA_PIN);
gpio_pull_up(EEPROM_SCL_PIN);

// Make the I2C pins available to picotool
// bi_decl(bi_2pins_with_func(EEPROM_SDA_PIN, EEPROM_SCL_PIN, GPIO_FUNC_I2C));
}


bool _cat24c256_write_page(uint16_t data_addr, uint8_t * data, size_t len){
uint8_t buf[len + 2]; // Include first two bytes for address
buf[0] = (data_addr >> 8) & 0xFF; // High byte of address
buf[1] = data_addr & 0xFF; // Low byte of address

// Copy data to buffer
memcpy(&buf[2], data, len);

// Send to the EEPROM
int ret;
ret = i2c_write_blocking(EEPROM_I2C, EEPROM_ADDR, buf, len + 2, false);
return ret != PICO_ERROR_GENERIC;
}


bool cat24c256_write(uint16_t base_addr, uint8_t * data, size_t len) {
uint16_t num_pages = len / PAGE_SIZE;
bool is_ok;

for (uint16_t page = 0; page <= num_pages; page += 1) {
uint16_t offset = page * PAGE_SIZE;
size_t write_size = PAGE_SIZE;
if (page == num_pages) {
write_size = len % PAGE_SIZE;
}

is_ok = _cat24c256_write_page(base_addr + offset, data + offset, write_size);
busy_wait_us(5 * 1000ULL);
if (!is_ok) {
return false;
}
}

return true;
}


bool cat24c256_read(uint16_t data_addr, uint8_t * data, size_t len) {
uint8_t buf[2]; // Include first two bytes for address
buf[0] = (data_addr >> 8) & 0xFF; // High byte of address
buf[1] = data_addr & 0xFF; // Low byte of address

i2c_write_blocking(EEPROM_I2C, EEPROM_ADDR, buf, 2, true);

int bytes_read;
bytes_read = i2c_read_blocking(EEPROM_I2C, EEPROM_ADDR, data, len, false);

return bytes_read == len;
}


bool cat24c256_eeprom_erase() {
uint8_t dummy_buffer[PAGE_SIZE];
memset(dummy_buffer, 0xff, PAGE_SIZE);


for (size_t page=0; page < 512; page++) {
size_t page_offset = page * PAGE_SIZE;
cat24c256_write(page_offset, dummy_buffer, PAGE_SIZE);
}
return true;
}
72 changes: 72 additions & 0 deletions display_minimal.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <u8g2.h>


#include "display_minimal.h"


// Local variables
u8g2_t display_handler;

u8g2_t * get_display_handler(void) {
return &display_handler;
}


void write_bl_info_AP(const char *wifi_ssid, const char *wifi_pass){
u8g2_SetMaxClipWindow(&display_handler);
u8g2_SetFont(&display_handler, u8g2_font_5x8_tr);
u8g2_DrawStr(&display_handler, 3, 15, "Bootloader in AP Mode");
u8g2_DrawLine(&display_handler, 3, 19, 125, 19);
u8g2_DrawStr(&display_handler, 3, 30, wifi_ssid);
u8g2_DrawStr(&display_handler, 3, 40, wifi_pass);
u8g2_DrawStr(&display_handler, 3, 50, "192.168.4.1:4242");
u8g2_UpdateDisplay(&display_handler);
}

void write_bl_info_STA(const char *wifi_ssid, bool status){
u8g2_SetMaxClipWindow(&display_handler);
u8g2_SetFont(&display_handler, u8g2_font_5x8_tr);
u8g2_DrawStr(&display_handler, 3, 15, "Bootloader in STA Mode");
u8g2_DrawLine(&display_handler, 3, 19, 125, 19);

if (status) {
u8g2_DrawStr(&display_handler, 3, 30, "Successfully connected to:");
u8g2_DrawStr(&display_handler, 3, 40, wifi_ssid);
u8g2_DrawStr(&display_handler, 3, 50, "Use serial-flash to");
u8g2_DrawStr(&display_handler, 3, 60, "update new app.elf.");
}
else {
u8g2_DrawStr(&display_handler, 3, 30, "FAILED connecting to:");
u8g2_DrawStr(&display_handler, 3, 40, wifi_ssid);
}

u8g2_UpdateDisplay(&display_handler);
}

/* u8g2 buffer structure can be decoded according to the description here:
https://github.com/olikraus/u8g2/wiki/u8g2reference#memory-structure-for-controller-with-u8x8-support

Here is the Python script helping to explain how u8g2 buffer are arranged.

with open(f, 'rb') as fp:
display_buffer = fp.read()

tile_width = 0x10 # 16 tiles per tile row

for tile_row_idx in range(8):
for bit in range(8):
# Each tile row includes 16 * 8 bytes
for byte_idx in range(tile_width * 8):
data_offset = byte_idx + tile_row_idx * tile_width * 8
data = display_buffer[data_offset]
if (1 << bit) & data:
print(' * ', end='')
else:
print(' ', end='')

print()

*/
23 changes: 23 additions & 0 deletions display_minimal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#ifndef DISPLAY_MINIMAL_H_
#define DISPLAY_MINIMAL_H_

#include <u8g2.h>
#include "pico/cyw43_arch.h"

#ifdef __cplusplus
extern "C" {
#endif

u8g2_t *get_display_handler(void);

void display_minimal_init(void);

void write_bl_info_AP(const char *wifi_ssid, const char *wifi_pass);
void write_bl_info_STA(const char *wifi_ssid, bool status);

#ifdef __cplusplus
}
#endif


#endif // DISPLAY_H_
Loading