Raspberry Pico: Designing a Custom C-SDK Library (Part 2)

Download the shift register library from Github: https://github.com/admantium-sg/rp2040-shift-register-74HC595

The Raspberry Pi Pico, or shorthand Pico, is a new microcontroller from the Raspberry Pi foundation. It provides a dual core ARM processor, 2MB of flash memory, and 26 GPIO pins. You can program the Pico with either a C/C++ SDK or MicroPython. I became fascinated by this device and started a new project: A library for connecting and controlling the HC595N shift register.

In the previous article, I explained the essentials of shift register operations, and detailed how the particular HC595N works. The article finished with an explanation of the libraries core objects and functions.

In this article, I continue the library development, and will detail my approach, development steps, and the final result.

This article originally appeared at my blog.

Development Approach

The HC595N library is my very first embedded software open-source project in the still new to me C language. My experience in prior projects shaped what I wanted to achieve with this library, and how it should be developed.

What’s important? I want to provide a library that captures the necessary operations to work with an HC595N library. The library objects and its functions — its API — should be clear and easy to use, an additional documentation should answer all questions. It should follow the development standard and code structuring principles of C. And finally, it should provide unit tests for providing implementation quality and a solid foundation for feature extensions and refactoring’s.

With this in mind, I decided to follow this approach:

Now, let’s see how these steps turned out in practice.

Essential Feature: Write a Single Bit

The ShiftRegister is a simple Struct object that defines all required pins and the two states serial_pin_state and shift_register_state.

typedef struct ShiftRegister
{
u_int8_t SERIAL_PIN;
u_int8_t SHIFT_REGISTER_CLOCK_PIN;
u_int8_t STORAGE_REGISTER_CLOCK_PIN;
bool serial_pin_state;
u_int8_t shift_register_state;
} ShiftRegister;

A ShiftRegister object can be initialized with a compound literal that maps to each of its pin.

ShiftRegister reg = {
.SERIAL_PIN=14,
.SHIFT_REGISTER_CLOCK_PIN=11,
. STORAGE_REGISTER_CLOCK_PIN=12
};

With this shift register created, we should now write a single bit. Since C has no objects, you typically define functions that receives a pointer to objects, and the arguments.

When called, this function will set the serial_pin_state to the passed argument of type bool. Then it will modify the register_state: If the passed bool is true, we effectively add 2 to the current register value. It its zero, we right-shift all bits by one.

static bool write_bit(ShiftRegister *reg, bool b)
{
reg->serial_pin_state = b;
(b) ? (reg->register_state += 0b10) : (reg->register_state <<= 0b01);
return true;
}

The test case writes two bits: 1 followed by 0. After each step, we test the pin_state is set correctly. Details can be found in my article about testing with CMocka.

void test_write_bit(void **state)
{
ShiftRegister reg = {14, 11, 12};
write_bit(1, &reg);
assert_int_equal(reg.serial_pin_state, 1);
write_bit(0, &reg);
assert_int_equal(reg.serial_pin_state, 0);
}

Hardware Test

When the software tests are running, it’s time to implement the hardware related functionality. At the time of writing this article, I was just beginning to develop microcontroller libraries, so my approach might not be the best yet.

From the specifics of the HC595N data sheet, we know that new data is written to the shift register when the clock pin goes high for one cycle. Whatever the state of the serial pin is — high or low — get written to the shift register. This means effectively we need to surround the existing code with statements that write data to the pins.

This looks like this:

static bool _write_bit(ShiftRegister *reg, bool b)
{
gpio_put(reg->SERIAL_PIN, b);
gpio_put(reg->SHIFT_REGISTER_CLOCK_PIN, 1);
reg->serial_pin_state = b;
(b) ? (reg->register_state += 0b10) : (reg->register_state <<= 0b01);
gpio_put(reg->SHIFT_REGISTER_CLOCK_PIN, 0);
gpio_put(reg->SERIAL_PIN, 0);
return true;
}

Line 3 writes the given bool value to the serial pin, and in Line 3 we set the shift register clock pin to 1. Line 7 and Line 8 put both pins back to 0.

With this addition, I uploaded the code and wrote a small example that would light 8 LEDs in succession, then turning them off again. With the proper teste circuit design on a breadboard, I could see this:

Structural Refactoring

So far, the essential function to write a single bit are implemented. They are supported by unit tests and a manual hardware test.

With this, we can continue to develop the library. The first thing however is to restructure the current code base: A proper header file in an include folder, the implementation in src, and a separate file in examples and test It looks like this:

├── examples
│ ├── 8_led_blink.c
├── include
│ └── admantium
│ └── rp2040_shift_register.h
├── src
│ ├── CMakeLists.txt
│ └── rp2040_shift_register.c
└── test
└── test.c
...

Extension with new Functions

With the structure in place, I then focused on adding new library functions. Each function was developed with the same approach:

In a short amount of time, I completed these functions:

Running Unit Tests without the Pico SDK

To get the unit tests working, I needed to resolve to a trick. The CMocka library is not compatible with the GCC ARM cross compiler, so the program needs to be compiled with the default C compiler, and then it cannot use the Pico SDK function. Therefore, I included a preprocessor flag to either load the Pico SDK or a set of mock functions:

#ifndef TEST_BUILD
#include <pico/stdlib.h>
#endif
#ifdef TEST_BUILD
#include <../test/mocks.h>
#endif

The mock functions are simple empty functions.

void stdio_init_all() { // do nothing }
void gpio_init(uint8_t gpio) { // do nothing }
static void gpio_set_dir(uint8_t gpio, bool out) { // do nothing }
static void gpio_put(uint8_t gpio, bool value) { // do nothing }

Note: CMocka provides a mechanism to wrap function calls as mocks. This requires a linker that works with --wrap <symbol> calls, see the man page. For example, you could compile with -Wl,--wrap=gpio_put to wrap the gpio_put function. But at the time of implementing the library, my knowledge of the C toolchain was too limited to get this working.

API Refactoring

The library is already in a good shape, now let’s make an API. My reference for C API Design is the book 21st Century C. The essence is: Create meaningful struct objects with data and functions, then provide global functions that take a pointer to these struct objects and call its methods. Providing these global functions is also the same pattern I have seen (and unknowingly used) in the Pico SDK. Also, it prevents to reference the struct object twice for a function call, like register.write_bit(&register, 1).

Therefore, the following new functions were added:

bool shift_register_write_bit(ShiftRegister *, bool);
bool shift_register_write_bitmask(ShiftRegister *, uint8_t);
bool shift_register_flush(ShiftRegister *);
bool shift_register_reset(ShiftRegister *);
bool shift_register_reset_storage(ShiftRegister *);
char *shift_register_print(ShiftRegister *);

Essentially, these functions work as dispatchers: They can call the structs method straight ahead, or they can e.g. check if the struct has the method, and if not, provide a default function. In the end, I decided to just call the struct methods directly. The implementation of the library function shift_register_write_bitmask() therefore is:

bool shift_register_write_bitmask(ShiftRegister *reg, u_int8_t btm)
{
return reg->write_bitmask(reg, btm);
}

Conclusion

Developing a shift register library for the Raspberry Pico followed a multi-phase approach: Bare metal proof-of-concept, restructuring, function extension, API wrapper. I had great fun in writing this library. This article detailed each of these phases. We learned how to interface the shift register with a pin config struct, and how to write a single bit to it. We then saw how to properly structure a library into the folders include, src, example, and test. Then I showed how to mock the Pico SDK functions when you can not compile with GCC ARM. Finally, we learned how to provide an API for the library. Now, go ahead and the try the library: Github pico-shift-register-74HC595.

IT Project Manager & Developer