Some code should be modular, shareable between projects. I define shareable as able to be (git) sub-moduled into a project. For example, say you want to write a shareable BLE host stack that works with different BLE modules and interfaces completely different application code? What if you need to talk to an off-chip module via SPI with one PCB, UART on another and interface functions when the application is on the same MCU/talks to the Network MCU via shared RAM (Nordic nRF5340 for example). Let’s make a diagram!

Kinda looks like a mini OSI model. But the OSI model is some abstract thing that you'll never actually implement because the smart people already have. Ok, maybe you did in school, but I'm a mechanical engineer so all I know is the refrigeration cycle.
Basic Implementation Problem
Looking at the stack, you can imagine the BLE Interface has a set of public functions that the Application can call; same with the Packet Translator, right? Well not really. The BLE Interface can have public "goes-outa" functions like a WriteCharacteristic() function, but how do you do the "goes into" without the BLE Interface calling application-specific code? Additionally, the BLE Packet Translator may not be the same on every PCB as modules from different companies will have unique communication protocols. So give up, copy-paste-modify as needed, right?
The Solution!
Copy-paste-modify equals no blog, so no. Welcome to the wonderful world of pointers and callbacks. But first, why not weak functions? The "weak" attribute can be used with a function declaration so that it can be overridden with a strong function declaration. You can use it to provide default function implementations, including empty functions that are just there to avoid compiler errors when no strong implementation exists. Other code is expected to override them with a strong implementation to customize as needed. For instance, you probably could declare a bunch of weak functions in the BLE Interface Layer and then strong declarations in the BLE Packet Translator layer. A Packet Translation Layer for a MicroChip RNDB451 module would have different strong implementations than one for a Nordic nRF52833. That would work, so why not use them? I don't like duplicate definitions. You get duplicates in your searches and your editor's goto function definition will probably get confused and ask "which definition?". Not the end of the world, but using function pointers avoids the issue.
The Code!
So exciting! Well, not really. Kinda disappointing compared to the actual BLE host stack I did back at the ranch, but that's IP so I can't post it. And it took a while and this is, let's face it, just a blog. So I share but a shadow of it's glory, which is actually easier to explain anyway. Visit the code on Github
The file names mostly match the stack layers, with SerialPort acting the part of the Physical Layer. Oscar worthy? Let's start with BleInterface.
BleInterface.h
/*Typedefs, enums and structure definitions*/
typedef enum _TE_PACKET_TRANSLATOR_RESP_ID
{
ptri_START_ADV,
ptri_WRITE_ATTRIBUTE,
ptri_TOTAL_PT_RESPONSE_IDS
}TE_PACKET_TRANSLATOR_RESP_ID;
typedef struct _TS_BLE_ATTRIBUTE_DATA
{
uint8_t *pData;
uint16_t startPos;
uint16_t dataLen;
uint16_t connectionHandle;
uint16_t attributeHandle;
}TS_BLE_ATTRIBUTE_DATA;
typedef struct _TS_PACKET_TRANSLATOR_CALLBACKS
{
void (*pStartAdv)(void);
void (*pWriteAttribute)(TS_BLE_ATTRIBUTE_DATA *pAttrData);
}TS_PACKET_TRANSLATOR_CALLBACKS;
typedef struct _TS_BLE_APP_CALLBACKS
{
void (*pStartAdvResp)(uint8_t isSuccess);
void (*pWriteAttributeResp)(uint8_t isSuccess);
}TS_BLE_APP_CALLBACKS;
/*Public function prototypes*/
void SetPacketTransCallbacks(const TS_PACKET_TRANSLATOR_CALLBACKS *pCallbacks);
void SetBleAppCallbacks(const TS_BLE_APP_CALLBACKS *pCallbacks);
void StartBleAdv(void);
void WriteBleAttribute(TS_BLE_ATTRIBUTE_DATA *pAttrData);
void PostBleResp(TE_PACKET_TRANSLATOR_RESP_ID respId, uint8_t isSuccess);
BleInterface is the glue layer between the application and the hardware. We don't want the application packing up the data in wonky hardware-specific ways and directly calling hardware functions; that's closely-coupled code that will need to be copy-paste-modified into the next project. Instead, the application calls, for instance, WriteBleAttribute(), passing a struct that includes everything needed in a non-hardware-specific format. BleInterface then invokes callback pWriteAttribute(), sending the data down the stack to whatever packet translator registered the callback. BleInterface does not need to know what file registered the callback, so it does not get bloated with multiple includes/function calls and logic for different packet translators. When the response to the command is received, the packet translator calls PostBleResp(), passing ID ptri_WRITE_ATTRIBUTE. Now I could have set this stack up so the translator invokes a callback for each response, but in the real stack, there are a bunch of responses and asynchronous events coming up and I thought it better to provide standard PostBleResp() and PostBleEvent() functions. The response handler invokes application callback pWriteAttributeResp() to complete the chain.
NordicPacketTranslator.h
/*Typedefs, enums and structure definitions*/
typedef struct _TS_NORDIC_PT_SETUP
{
uint32_t (*pSendBytes)(uint8_t *pBuf, uint32_t numBytes);
uint32_t (*pGetBytes)(uint8_t *pBuf, uint32_t numBytes);
}TS_NORDIC_PT_SETUP;
/*Public function prototypes*/
void InitNordicPacketTranslator(const TS_NORDIC_PT_SETUP *pSetup);
void UpdateNordicPacketTranslator(void);
NordicPacketTranslator registers the TS_PACKET_TRANSLATOR_CALLBACKS and then takes standard format command data coming down from BleInterface and packs it into the AT command set used by the Nordic BLE module; note that I'm making up the AT commands. The application code calls InitNordicPacketTranslator() to register callbacks pSendBytes and pGetBytes. NordicPacketTranslator invokes these to send/get bytes from whatever physical interface is being used.
The Conclusion!
I believe that I have coined a phrase; Simplicated. Or possibly Complimple...I think simplicated is way better. Anyway, Simplicated is defined as simple to use from the outside but a giant ball of pointers on the inside. This modular code stuff falls into that category, especially when you start handling multiple simultaneous operations...probably should blog on that someday. Anyway, try it out next time you have a need. Or, if not, start by breaking your code up into functions and avoid global variables. Make shared utility functions for repeated operations. Use a callback for something asynchronous. Soon you'll have a stack to call your own.