Initializing Microchip’s BM78 module

In my previous posts BM78 Bluetooth module – First glance and BM78 Commands and Events in Application mode we got to know the BM78 module and learned how to communicate with it in Application-Mode in Manual-Pattern.

In this post we will build a bit of a framework which will help in the future to setup new projects based on this module more quickly.

Module’s status

In the previous post we introduced an Initialization-Mode, a software sub-mode to the Application-Mode. In this mode we want to initialize the BM78 module and hold its status for the MCU to have access to it before we start working with the BM78 module. For that purpose we define the following structure:

struct {
    BM78_Mode_t mode;                      // Dongle mode.
    BM78_Status_t status;                  // Application mode status.
    bool enforceStandBy;                   // Whether to enforce Stand-By mode
                                           // on disconnect or idle.
    BM78_PairingMode_t pairingMode;        // Pairing mode.
    uint8_t pairedDevicesCount;            // Paired devices count.
    BM78_PairedDevice_t pairedDevices[16]; // Paired devices MAC addresses.
    char deviceName[17];                   // Device name.
    char pin[7];                           // PIN.
} BM78 = {BM78_MODE_INIT, BM78_STATUS_POWER_ON, true, BM78_PAIRING_PIN, 0};

We want to maintain the mode, status, pairingMode, deviceName, pin and pairedDevices variables at all times. They should be first set in the Initialization-Mode and than maintained through the whole lifecycle.

The mode (BM78_Mode_t) start in BM78_MODE_INIT mode and can have one of the following values:

typedef enum {
    BM78_MODE_INIT = 0x00,
    BM78_MODE_APP  = 0x01,
    BM78_MODE_TEST = 0xFF
} BM78_Mode_t;

which correspond with the three states the BM78 module can be in.

The status (BM78_Status_t) starts in the BM78_STATUS_POWER_ON state and can have one of the following values, see:

typedef enum {
    BM78_STATUS_POWER_ON = 0x00,
    BM78_STATUS_PAGE_MODE = 0x02,
    BM78_STATUS_STANDBY_MODE = 0x03,
    BM78_STATUS_LINK_BACK_MODE = 0x04,
    BM78_STATUS_SPP_CONNECTED_MODE = 0x07,
    BM78_STATUS_LE_CONNECTED_MODE = 0x08,
    BM78_STATUS_IDLE_MODE = 0x09,
    BM78_STATUS_SHUTDOWN_MODE = 0x0A
} BM78_Status_t;

This corresponds to the BM78’s internal state machine:

Next, the paringMode (BM78_PairingMode_t) with BM78_PAIRING_PIN as the default value represents the pairing method of the BM78 module and can have one of the following values. As documented in:

typedef enum {
    BM78_PAIRING_PIN = 0x00,
    BM78_PAIRING_JUST_WORK = 0x01,
    BM78_PAIRING_PASSKEY = 0x02,
    BM78_PAIRING_USER_CONFIRM = 0x03
} BM78_PairingMode_t

Next two attributes, the deviceName and the pin, hold the device’s bluetooth name and the PIN number as a string value.

Note that the limit on those values is the size of the char array -1 since we use the \0 in addition here to terminate the string. The limit of deviceName is 16 chars and the limit of a PIN value is 6 numbers.

Last is the pairedDevices (incl. pairedDevicesCount) attribute.

Initializing the module

When the BM78 module get powered up or reset it send an BM78_EVENT_BM77_STATUS_REPORT event. So we will start our initialization waiting for this event and define 12 initialization stages. We can start our initialization with BM78_init.keep or without it. If BM78_init.keep = true than all settings (paringMode, deviceName, pin) will be overwritten by values stored in the BM78 module. Otherwise in case the provided values do not match with the ones stored in the BM78 module corresponding overwrite commands will be issued.

  1. BM78_CMD_READ_LOCAL_INFORMATION: will read BM78’s informations (MAC address, version)
  2. BM78_CMD_READ_DEVICE_NAME: will read stored Bluetooth Name. If BM78_init.keep = true then the BM78.deviceName will be overwritten with the value returned and we continue with stage 5. If BM78_init.keep = false and the value returned matches BM78.deviceName we continue with stage 5. If the values do not match we continue with state 3.
  3. BM78_CMD_WRITE_DEVICE_NAME: will write the BM78.deviceName value to the BM78 module’s EEPROM.
  4. BM78_CMD_WRITE_ADV_DATA: will write the BM78.deviceName value to BM78’s advertising data in the EEPROM. Continues with stage 2 to check the written value.
  5. BM78_CMD_READ_PAIRING_MODE_SETTING: will read stored Bluetooth’s pairing mode. If BM78_init.keep = true then the BM78.pairingMode will be overwritten with the value returned and we continue with stage 7. If BM78_init.keep = false and the value returned matches BM78.pairingMode we continue with stage 7. If the values do not match we continue with state 6.
  6. BM78_CMD_WRITE_PAIRING_MODE_SETTING: writes the desired pairing mode to the BM78’s EEPROM/
  7. BM78_CMD_READ_PIN_CODE: will read stored Bluetooth PIN. If BM78_init.keep = true then the BM78.pin will be overwritten with the value returned and we continue with stage 9. If BM78_init.keep = false and the value returned matches BM78.pin we continue with stage 9. If the values do not match we continue with state 8.
  8. BM78_CMD_WRITE_PIN_CODE: will write the BM78.pin value to the BM78 module’s EEPROM. Continues with stage 7 to check the written value.
  9. BM78_CMD_READ_ALL_PAIRED_DEVICE_INFO: will read all paired devices and write the result into BM78.pairedDevices and BM78.pairedDevicesCount. If the BM78.pairedDevicesCount == 0 we continue with stage 10. In case we already have some paired devices we continue with stage 11.
  10. BM78_CMD_INVISIBLE_SETTING: will enter to BM78_STATUS_STANDBY_MODE connectable to any device and continues with stage 12.
  11. BM78_CMD_INVISIBLE_SETTING: will enter to BM78_STATUS_STANDBY_MODE connectable only to paired/trusted devices and continues with stage 12.
  12. Finish

Since the asynchronous communication is not 100% we need to take care when commands were not sent properly or events received. For that purpose we defined the above stages and are holding progress in:

union {
    struct {
        uint8_t stage  :4; // max 16 stages
        uint8_t attempt:3; // max 7 attempts
        bool keep      :1;
    };
} BM78_init = { 0, 0, false };

The function BM78_checkState should be called in regular intervals using a timer to check the current state and take action if something hangs. The part relevant for the initialization:

void BM78_checkState(void) {
    switch (BM78.mode) {
        case BM78_MODE_INIT: // In Initialization mode retry setting up the BM78
                             // in a defined interval
            if (BM78_counters.idle > (BM78_INITIALIZATION_TIMEOUT / BM78_TRIGGER_PERIOD)) {
                BM78_counters.idle = 0; // Reset idle counter.
                BM78_setup(BM78_init.keep, BM78.deviceName, BM78.pin, BM78.pairingMode);
            }
            break;
        // ...
    }
    BM78_counters.idle++; // Idle counter increment.
}

is calling the BM78_setup() over and over till it leaves the BM78_MODE_INIT mode.

void BM78_setup(bool keep, char *deviceName, char *pin,
                BM78_PairingMode_t pairingMode) {
    BM78_init.keep = keep;
    strcpy(BM78.deviceName, deviceName, min(strlen(deviceName), sizeof(BM78.deviceName)));
    BM78.deviceName[16] = '\0';
    strcpy(BM78.pin, pin, min(strlen(pin), sizeof(BM78.pin)));
    BM78.pin[6] = '\0';
    BM78.pairingMode = pairingMode;
    switch (BM78.mode) {
        case BM78_MODE_INIT:
            if (BM78_init.attempt < 7 && BM78_init.stage > 0) {
                BM78_init.attempt++;
                while(UART_isRXReady()) UART_read();
                if (BM78_init.attempt == 3) BM78_reset(); // Soft reset
                BM78_state = BM78_STATE_IDLE;
                BM78_retryInitialization();
                break;
            }
            // no break here
        case BM78_MODE_APP:
            while(UART_isRXReady()) UART_read();
            BM78_state = BM78_STATE_IDLE;
            BM78.mode = BM78_MODE_INIT;
            BM78_counters.idle = 0;
            BM78_counters.missedStatusUpdate = 0;
            BM78_init.stage = 0;
            BM78_init.attempt = 0;
            BM78_tx.length = 0; // Abort ongoing transmission
            BM78_power(true);
            BM78_reset();
            break;
    }
}

In BM78_MODE_INIT mode the BM78_setup() function tries 7 times to resent the same command, after 3rd time it issues a soft-reset, after 7th time it starts the initialization from scratch, including clearing the UART RX buffer.

The stages mentioned above are all present in the BM78_retryInitialization() function.

void BM78_retryInitialization(void) {
    switch (BM78_init.stage) {
        case 1:
            BM78_execute(BM78_CMD_READ_LOCAL_INFORMATION, 0);
            break;
        case 2:
            BM78_execute(BM78_CMD_READ_DEVICE_NAME, 0);
            break;
        case 3:
            BM78_write(BM78_CMD_WRITE_DEVICE_NAME, BM78_EEPROM_STORE,
strlen(BM78.deviceName), BM78.deviceName); break; case 4: BM78_GetAdvData(); BM78_write(BM78_CMD_WRITE_ADV_DATA, BM78_EEPROM_STORE, 22, BM78_advData); break; case 5: BM78_execute(BM78_CMD_READ_PAIRING_MODE_SETTING, 0); break; case 6: BM78_execute(BM78_CMD_WRITE_PAIRING_MODE_SETTING, 2, BM78_EEPROM_STORE,
BM78.pairingMode); break; case 7: BM78_execute(BM78_CMD_READ_PIN_CODE, 0); break; case 8: BM78_write(BM78_CMD_WRITE_PIN_CODE, BM78_EEPROM_STORE, strlen(BM78.pin),
BM78.pin); break; case 9: BM78_execute(BM78_CMD_READ_ALL_PAIRED_DEVICE_INFO, 0); break; case 10: BM78_execute(BM78_CMD_INVISIBLE_SETTING, 1, BM78_STANDBY_MODE_ENTER); break; case 11: BM78_execute(BM78_CMD_INVISIBLE_SETTING, 1,
BM78_STANDBY_MODE_ENTER_ONLY_TRUSTED); break; case 12: BM78_init.stage = 0; BM78_init.attempt = 0; BM78.mode = BM78_MODE_APP; break; } }

The flow is implemented in BM78_AsyncEventResponse(). Please see https://github.com/kubovy/mclib/blob/master/modules/bm78.c for full implementation. There are 3 function important regarding listening to events:

First, the BM78_checkNewDataAsync() function which should be called from the main loop as much as possible. This function actually reads bytes from the UART RX buffer and calls one of the processing functions.

Second, the mentioned BM78_processByteInAppMode() function, which operates on a byte level and builds up the BM78_rx.response structure and the BM78_rx.buffer. The BM78_rx.response holds basic event data according to:

The BM78_rx.buffer holds additional data not fitting the structure, e.g., arbitrary transparent data from another device. When a complete event is build up this function calls the BM78_AsyncEventResponse().

Last, BM78_AsyncEventResponse() which operates on the event level and reacts to specific events. It also calls additional handlers (BM78_eventHandler, BM78_errorHandler) which can be implemented outside this file to allow your application use this parsing.

Conclusion

I had some troubles to make the implementation stable. Sometimes it got stuck. I tried to simplify the code for readability and optimize it for data and program memory usage. Nevertheless there is still some potential. Check the full code at https://github.com/kubovy/mclib/blob/master/modules/bm78.c. The BM78_init.keep is meant for devices to fully initialize first time and store this fact into some persistent memory (even just 1bit). Next time the PIC starts it will read this bit and set BM78_init.keep = true making the initialization process lighter. Also you may not want to rewrite the BM78’s EEPROM all the time so it does not ware-out.

Leave a Reply

Your email address will not be published. Required fields are marked *