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

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:

  • Write a single C file with all the necessary code for the very first feature, keeping in mind to provide semantically named objects and functions
  • Software Test: Add unit tests that completely cover this initial feature set
  • Hardware test: Build a breadboard with the microcontroller, the shift register, and outputs devices (LEDs etc.) and test your code
  • Switch to test driven development by writing a unit test first before a new function, finalizing all essential features
  • Adept to good API design guidelines and C coding standards: Restructure the project into separate header, implementation, and test files. And provide conventional ways to create new objects and library functions (both object-scoped and global)
  • Restructure the project into C coding standards: A header file, an implementation file, and a test file. Also, upgrade the library to
  • Provide mocks or stubbing to all hardware related functions to test the library in isolation of any concrete hardware
  • Add the Hardware specific standard compilation stack that’s accepted in the community

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

Essential Feature: Write a Single Bit

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

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

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

  • Add a definition to the header file
  • Add a new test
  • Provide the implementation
  • Test & debug

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

  • write_bit(ShiftRegister *, bool): Write a single bit to the shift register, and perform a shift-right of all other bits.
  • write_bitmask(ShiftRegister *, u_int8_t): Write a complete bitmask, e.g. 0b10101010 to the register.
  • flush(ShiftRegister *): Flush the content of the shift register to the storage register.
  • reset(ShiftRegister *): Resets the shift register's content to bitmask 0b00000000.
  • reset_storage(ShiftRegister *) Resets the storage register's content to bitmask 0b00000000 and performs a shift_register_flush()
  • char * shift_register_print(ShiftRegister \*) Prints the shift register's state as a bitmask, and returns a char* of the bitmask string.

Running Unit Tests without the Pico SDK

#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

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

IT Project Manager & Developer