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:
- 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
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, ®);
assert_int_equal(reg.serial_pin_state, 1);
write_bit(0, ®);
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:
- 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 bitmask0b00000000
.reset_storage(ShiftRegister *)
Resets the storage register's content to bitmask0b00000000
and performs ashift_register_flush()
char * shift_register_print(ShiftRegister \*)
Prints the shift register's state as a bitmask, and returns achar*
of the bitmask string.
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 thegpio_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(®ister, 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.