Looks great and definitely a very interesting project. I have very little experience with C++ features. Can you write a post about your "carzy constexpr hackery"? I'd be interested. I am in no position to review the code. I think I will learn a lot with your firmware though.
Btw, is that ground pour on the top layer? May I ask the reason for it? I am also still learning PCB design..
Ok, so 'crazy constexpr hackery'. You asked for it, wall of text incoming...
Basically, in the past, you could do quite a lot of compile-time calculations in C++ using templates, which essentially form their own Turing complete meta-programming language.
There's a bunch of libraries out there such as sprout and boost::mpl that allow you to create compile-time data structures, but templates in general, and template meta-programming in particular, can lead to some very complex and non-self-explanatory error messages, especially because everything revolves around types rather than actual data.
Since C++11/14, however, we now have a restricted, but rather functional, implementation of compile-time function evaluation (CTFE) referred to as constexpr functions. C++11 had many more restrictions on what could go into a constexpr function, but almost everything is fair game if your compiler is C++14 compliant (clang, gcc 5.2.0+, icc I think are the main ones claiming compliance).
constexpr doesn't just apply to functions, it can apply to data variables as well, and it basically tells the compiler that it can evaluate the function (in the case of a constexpr function) or evaluate the assignment statement (in a constexpr variable declaration) at compile-time rather than during the execution of the program.
The relevance for microcontrollers, and in turn keyboard controllers, is that data which doesn't change during the execution of the program
or the results of functions that can be calculated using values known at compile time, can be stored in the flash memory rather than being dynamically allocated and stored in RAM, which we have much less of than we do flash, and it avoids potential memory fragmentation issues due to dynamic allocation which would otherwise require writing custom allocators/custom new operators, implementing memory pools, etc etc.
The reduced RAM requirements also help make the firmware portable to a wider variety of microcontrollers, because we only need a much more limited amount of RAM for the firmware.
Constexpr functions can be combined with other new features such as function parameter deduction. Here's a small excerpt:
template <class T, size_t N>
constexpr auto make_constexpr_array(T const(&Array)[N])
{
constexpr_array<T, N> TmpArray = constexpr_array<T, N>();
for (int i = 0; i < N; i++)
{
TmpArray.Data[i] = Array[i];
}
return TmpArray;
};
This function uses parameter deduction to determine the size of the array to allocate.
If I call the function like this:
constexpr const auto TestCharacterArray = make_constexpr_array("WCS");
then a few things happen. Firstly, because I'm providing a string literal without prefixes, "WCS" is considered a char array. This means that, using the template parameter deduction rules, I don't need to specify values for <class T, size_t N> when I run the function because the compiler says 'You've given me an array of char with length 3, so I'll just fill in those template values for you'.
This means inside the function, where you can see me declaring the array TmpArray, I can simply pass those parameters through to my inner template. I can then copy the values from the array the user passed in, in the loop, into my new object, and then I can return it.
The actual 'constexpr_array' class conforms to the C++14 'concept' called 'literal type' which means that I am not returning a reference to TmpArray, I'm actually returning by value, so I don't need to worry about returning data allocated on the stack (inside the function).
By declaring the function as auto, and then my 'TestCharacterArray' as auto, the compiler can perform type deduction on the function. ie it looks at the type of TmpArray, and sets that as the return type of make_constexpr_array, and in turn deduces the type of TestCharacterArray, so I don't need to manually count the items in my array input and use that to create a variable to store the return value of the function in.
Likewise, if I call
constexpr auto TestCharacterArray = make_constexpr_array(u"WCS");
then the compiler interprets "WCS" as a two-byte/uint16_t-backed string, and so it deduces my invocation of the function to be
make_constexpr_array<uint16_t, 3>(u"WCS");
and in turn TestCharacterArray will actually be a constexpr_array storing 'uint16_t's instead of plain char.
More to the point, because TestCharacterArray is declared 'constexpr' the compiler *can* if it wants, run the function at compile-time and simply place the computed value into the constant data section so the whole array will go into flash memory.
Putting this all together with some helper functions that wrap the idea of a constexpr_array means I can create things with much nicer syntax.
For example, here's the traditional way to represent a collection of USB String Descriptors:
ALIGNED(4) uint8_t USB_StringDescriptors[] =
{
0x04,
USB_STRING_DESCRIPTOR_TYPE,
WBVAL(0x0409),
//Manufacturer
(3 * 2 + 2),
USB_STRING_DESCRIPTOR_TYPE,
'W', 0,
'C', 0,
'S', 0,
//Product
(8 * 2 + 2),
USB_STRING_DESCRIPTOR_TYPE,
'K', 0,
'E', 0,
'Y', 0,
'B', 0,
'O', 0,
'A', 0,
'R', 0,
'D', 0,
//Serial
(4 * 2 + 2),
USB_STRING_DESCRIPTOR_TYPE,
'0', 0,
'0', 0,
'0', 0,
'1', 0
};
And here's my much cleaner syntax for creating the exact same thing in memory (verified with a hex editor that it creates the same bytestream):
ALIGNED(4) static constexpr auto MyDescriptors =
USB_String_Descriptor_Collection().Append(StringDescriptor("WCS"))
.Append(StringDescriptor("KEYBOARD"))
.Append(StringDescriptor("0001"));
(I'll probably clean the syntax further, I should be able to do away with Append and simply use .StringDescriptor, but you get the idea).
So the firmware uses a bunch of that sort of stuff in the back-end but I can perform much nicer sanity-checking than a traditional compile-time implementation using templates could use.
As a result, most of the complexity that you see here should be hidden from people using the firmware, but if people want to get into modifying or extending it, constexpr functions have a far more natural syntax than template meta-programming, so it should still be relatively easy for people to extend.
As far as the copper pour goes, I find that having copper ground pours on top and bottom dramatically decrease the complexity of my routing.