Raspberry Pi Microcontrollers
Pico Series
- Raspberry Pi Pico and Pico W
- Dual Arm Cortex-M0+ with Armv6-M programming model, up to 133 MHz
- Raspberry Pi Pico 2 and Pico 2 W
- Dual Arm Cortex-M33 with Armv8-M programming model, up to 150 MHz
Development Setup
Using the Raspberry Pi Debug Probe
The Debug Probe is a RP2040 based USB-to-UART/SWD board for connecting the USB slot of a development machine to the SWD or UART pins of a target device.
Using A Pico To Debug Another Pico
A Pico can be used to debug another Pico (a.k.a. target device) by flashing it with the debugprobe binary and wiring to the target Pico as shown below.
From the debugger Pico to the target Pico,
- pin #4 (GP2) connects to pin SWCLK,
- pin #5 (GP3) connects to SWDIO,
- pin #6 (GP4, UART1 TX) connects to pin #2 (GP1, UART0 RX),
- pin #7 (GP5, UART1 RX) connects to pin #1 (GP0, UART0, TX).
The debugger Pico serves both as a USB-to-SWD and USB-to-UART bridge to the target device. The development machine, therefore, needs to be connected via USB only to the debugger Pico. Console output on the target Pico is relayed through the debugger. It is possible, though, to connect a second USB cable from the development machine to the target device for (simultaneous) direct access to the console output. This debugger setup works not just with Pico targets but with any microcontroller that supports the CMSIS-DAP protocol over SWD.
Software Tools for Debugging
Whether we use the Raspberry Pi Debug Probe or a spare Pico as debug probe, the software tools used for debugging are the same:
- openocd
Notes on Programming with the C SDK
Async Context
In the C SDK, an async_context is used in concurrent programming to make sure that functions are executed on the same thread and in the order they were submitted, similar to dispatch queues in Apple's Dispatch framework. The async_context data structure is a container of linked lists of worker functions that are executed within a single thread of a single processor core.
Bluetooth with BTstack
The C SDK integrates the third-party BTstack framework as its Bluetooth API that interfaces with the Infineon CYW43 wireless communication module (on Pico W models) via the Host-Controller Interface (HCI) protocol.
Setup
Before we can call any BTstack functions (and hope for some meaningful outcome), we need to add a btstack_config.h header file to our project. In it, we add all the compile-time switches that fit our application. On a microcontroller, a frequent use case for BTstack is to implement a BLE peripheral. For this we, our config header should look something like this:
#ifndef MY_BTSTACK_CONFIG_H
#define MY_BTSTACK_CONFIG_H
#ifndef ENABLE_BLE
#error In CMakeLists.txt add the 'pico_btstack_ble' library to target_link_libraries(...)
#endif
// logging
#define ENABLE_LOG_INFO
#define ENABLE_LOG_ERROR
#define ENABLE_PRINTF_HEXDUMP // required for hci_dump_init()
#if 1 // configure as peripheral (GATT server)
#define ENABLE_LE_PERIPHERAL
#define MAX_NR_GATT_CLIENTS 0
#else // configure as central (GATT client)
#define ENABLE_LE_CENTRAL
#define MAX_NR_GATT_CLIENTS 1
//#define ENABLE_GATT_CLIENT_PAIRING
#endif
// Setting various buffer sizes related to HCI communication
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
#define HCI_ACL_PAYLOAD_SIZE (255 + 4)
#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4
#define MAX_NR_HCI_CONNECTIONS 1
#define MAX_NR_SM_LOOKUP_ENTRIES 3
#define MAX_NR_WHITELIST_ENTRIES 16
#define MAX_NR_LE_DEVICE_DB_ENTRIES 16
// Choosing a fixed-size attributes table over using malloc.
// We want to use BTstack without heap memory allocations.
#if 1
#define MAX_ATT_DB_SIZE 512
#else
#define HAVE_MALLOC
#endif
// Enabling timers
#if 1
#define HAVE_EMBEDDED_TIME_MS
#else
#define HAVE_EMBEDDED_TICK
#endif
// Limiting the amount of nonvolatile (flash) memory used for storing peer bonding information
#define NVM_NUM_DEVICE_DB_ENTRIES 16
#define NVM_NUM_LINK_KEYS 16
// Configuring HCI Controller-to-Host flow control and related buffers to avoid overrunning the cyw43 shared bus
#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL
#define HCI_HOST_ACL_PACKET_LEN (255+4)
#define HCI_HOST_ACL_PACKET_NUM 3
#define HCI_HOST_SCO_PACKET_LEN 120
#define HCI_HOST_SCO_PACKET_NUM 3
#define MAX_NR_CONTROLLER_SCO_PACKETS 3
#define MAX_NR_CONTROLLER_ACL_BUFFERS 3
// Enabling btstack_assert, acting just like normal assert()
#define HAVE_ASSERT
// Setting HCI resend timeout in case the Bluetooth module needs more time to response.
#define HCI_RESET_RESEND_TIMEOUT_MS 800
// Configuring security
#define ENABLE_SOFTWARE_AES128
#define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS
#endif // MY_BTSTACK_CONFIG_H
GATT Profiles, Services, Characteristics, and Descriptors
A Bluetooth Low Energy Peripheral organizes the served data in a linear attributes table or database, referred to as ATT. The GATT (Generic Attributes) profile used in BLE is, contrary to what the name suggests, a specific organizational structure imposed on the ATT table, where the data table as a whole is referred to as "profile". The profile attributes can be broken down into consecutive "service" attributes, the service attributes into "characteristic" attributes, the characteristics into "descriptor" attributes. So, all in all there linear ATT table becomes a tree structure with four levels in GATT. A BLE peripheral, acting as a GATT server when connected to, will advertise its profile over GAP. A BLE central, acting as a GATT client after establishing a connection to the peripheral, can read the complete GATT hierarchy to find out about the profile contents.
BTstack allows defining a GATT table as a human-readable text file. The translation of that file into a C header by a Python script is a build step that can be included in CMakeLists.txt using the function pico_btstack_make_gatt_header(). The generated C header contains the GATT table as an array of hex-coded bytes, laid out one line per attribute, with comments between the lines for orientation. Application code that needs to access the GATT table data then simply "#include"s this generated header file.
When defining the GATT table in BTstack's text format, we can use the following keywords for the attribute permissions of a characteristic:
- READ
- WRITE
- WRITE_WITH_RESPONSE
- NOTIFY: Adds a Client Configuration descriptor to the characteristic that allows a GATT client to control whether a notification is to be sent to the client each time the value of the characteristic changes.
- DYNAMIC: Indicates that reading and writing to this characteristic requires adding this case to the read and write callbacks.
BTStack Python script output for a GATT characteristic declaration
Let's have a look at the output of the BTstack Python script that converts the .gatt file to a C header and let's narrow it down to just the characteristic declarations.
Here is the binary sequence (in hex representation) generated for an attribute that declares a GATT characteristic with attribute UUID B0B0F155-B0B0-B0B0-B0B0-B0B000000101 and permissions READ | DYNAMIC
0x1b, 0x00, 0x02, 0x00, 0x08, 0x00, 0x03, 0x28, 0x02, 0x09, 0x00, 0x01, 0x01, 0x00, 0x00, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0xb0, 0x55, 0xf1, 0xb0, 0xb0
The bytes are grouped from left to right according to this scheme:
- 2 bytes for the length of this attribute definition: 0x001b (= 27 bytes)
- 2 bytes for the access permission of this attribute: 0x0002 (= read-only)
- 2 bytes for the attribute handle: 0x0008
- 2 bytes for the attribute type: 0x2803 (short 16-bit UUID that indicates a characteristic declaration)
- 1 byte for the access permissions of the declared characteristic: 0x02 (= read-only)
- 2 bytes for the attribute handle of the characteristic value: 0x0009
- 16 bytes for the 128-bit UUID of the characteristic: 0xb0b0f155b0b0b0b0b0b0b0b000000101
where the bytes in each group are in reverse order!
For the official specification of the Bluetooth 5.4 GATT characteristic declaration refer to section 3.3.1 in the in the Bluetooth 5.4 Core Specification. That section also lists the characteristics permissions (or properties):
Properties | Value | Description |
---|---|---|
Broadcast | 0x01 | If set, permits broadcasts of the Characteristic Value using Server Characteristic Configuration Descriptor. If set, the Server Characteristic Configuration Descriptor shall exist. |
Read | 0x02 | If set, permits reads of the Characteristic Value using procedures defined in Section 4.8 |
Write Without Response | 0x04 | If set, permit writes of the Characteristic Value without response using procedures defined in Section 4.9.1. |
Write | 0x08 | If set, permits writes of the Characteristic Value with response using procedures defined in Section 4.9.3 or Section 4.9.4. |
Notify | 0x10 | If set, permits notifications of a Characteristic Value without acknowledgment using the procedure defined in Section 4.10. If set, the Client Characteristic Configuration Descriptor shall exist. |
Indicate | 0x20 | If set, permits indications of a Characteristic Value with acknowledgment using the procedure defined in Section 4.11. If set, the Client Characteristic Configuration Descriptor shall exist. |
Authenticated Signed Writes | 0x40 | If set, permits signed writes to the Characteristic Value using the procedure defined in Section 4.9.2. |
Extended Properties | 0x80 | If set, additional characteristic properties are defined in the Characteristic Extended Properties Descriptor defined in Section 3.3.3.1. If set, the Characteristic Extended Properties Descriptor shall exist. |
The BTstack runloop
The following articles by Hunter Adams offer a detailed analysis of how BTstack works on the Pico:
- BTstack and RP2040: HCI (Host Controller Interface).
This article delves, among other things, into how the BTstack runloop is executed inside a Pico async_context.[...] we have just one when_pending_worker in our async_context. This worker is called cyw43_poll_worker, and its do_work function points to cyw43_poll_func(), listed above. This function ends up calling btstack_run_loop_poll_data_sources_from_irq(), about which we'll learn more shortly. This worker gets marked as pending in a GPIO interrupt. So, this provides a mechanism by which the CYW43 can tell the RP2040 to run btstack_run_loop_poll_data_sources_from_irq() which, as we're going to learn, goes and gets data from the device.
- Building a Bluetooth GATT Server on the Pi Pico W
GATT Characteristic read and write operations
The struct att_service_handler_t
represents a GATT service. After the fields of the struct have been filled in by the application code, it holds all the information needed for the BLE peripheral to make it available to the BLE central. Some of the fields of the struct hold the ATT handles that define the start and end of the collection of characteristics belonging to the service. Other fields of the struct (read_callback
and write_callback
) hold pointers to functions that handle reading from and writing to characteristic values.
Note that the read callback for a descriptor of a GATT characteristic (i.e., an ATT attribute) is called twice. In the first call, the buffer pointer passed to the callback function is NULL. In the second call, it's a valid pointer. The first call is made by BTstack to query the size of the data that will be returned by the second call. BTstack needs to make sure its transmission buffers can hold the amount of data before making the second call.
At the end of a writing operation, if the characteristic was configured to send back notifications to the BLE peer, one more callback function comes into play. It is part of another struct, btstack_context_callback_registration_t
, that is used for the deferred sending of characteristic update notification to the BLE central. In this callback, att_server_notify()
is called to finally send the write notification to the peer.