cansw_processor_stm
Software repo for the processor board
Processor is a bit different from most other rocketCAN boards as it uses an STM32H733 processor. The development environment of choice is STM's own STM32CubeIDE.
To get started with development, clone the repository and open the IDE project folder in the workspace directory of your choice. You will also need to initialize the submodules in the project using
git submodule update --init --recursive
Otherwise this process is fairly standard across Eclipse-based IDEs, so any issues should be resolvable with some Google-fu.
Development Guidelines
RTOS
In addition to targeting a more modern CPU, the project is made significantly more complicated by the use of a real-time operating system (RTOS). Our RTOS of choice is FreeRTOS, a lightweight and very functional choice. What this means for you as a developer is, number one, RTFM. Number two, do not put your application code in main.c
. Put one or more source and header files inside of Tasks/
, and only import headers, call an initialization function, and add your task to the RTOS scheduler. Some other basic RTOS guidelines
- Never ever ever allow your task function to exit. if you absolutely need to stop your task from running any more, you can call a kernel function that kills the task from inside itself. In general, you should not need to dynamically spawn and kill tasks. If you are, you are probably doing something wrong.
- FreeRTOS uses a time-based round robin scheduler with prioritization. This means if you don't force your task to yield (either via explicit call or a function call that blocks), and it has higher prioritization than a given thread, the other thread will never run. Use the "normal" priority when adding a task to the scheduler, and if you must set a higher priority, it MUST use some sort of blocking mechanism (xQueueReceive is a common one). Be extremely careful if using a time-based block like xTaskDelay on a higher-than-normal priority task.
- If you need to pass a small to medium amount of data between threads and/or ISRs, use a Queue. These are thread safe, ISR safe, and let you very easily block and unblock tasks to perform many common operations. Having multiple threads access the same memory region without some form of synchronization is super memory-unsafe. don't do it.
- If you need to pass a large amount of data between threads, don't use a queue. This is because queues pass by copy rather than reference, which means a ton of RAM overhead. Use a shared memory region and a mutex (which FreeRTOS also supports). If you need to manage multiple large memory areas, you can use queues to implement a ring buffer or similar
- FreeRTOS has a dynamic memory allocator. Do not under any circumstances use it for arbitrary memory allocation in your tasks. Dynamic memory allocation is acceptable for thread stacks/queues (where it happens autotmatically if you use the non-static API functions) since the OS will immediately and obviously die if it does not have enough memory at initialization.
- FreeRTOS has event flags. If you need to signal events between threads, use these since they block/unblock instantly, as opposed to writing and polling shared memory regions using time delays
- When writing a periodic task (one that needs to run every X seconds), use xTaskDelayUntil rather than xTaskDelay. This is because xTaskDelayUntil takes into account the running time of the code itself
- When setting the delay for a periodic task, delcare the intended task frequency in Hz and then convert that using the
portTICK_RATE_MS
macro to an equivalent number of ticks, and pass that value to the delaying function. This makes the running frequency tick rate-agnostic and makes it easier for someone who wants to change the running frequency of the task on the fly.
Hardware
- If you need to use a peripheral,
extern
the HAL handle generated by the code configurator in your header file.
- This is more of a stylistic choice, but only one thread should ever interact with a peripheral. This saves everyone the trouble of calling a HAL function, seeing that the peripheral is blocked, and needing to loop back around.
- If you need to use a callback function for a peripheral that doesn't have unique callbacks (such as UART), do not override the
__weak
generic callback. Instead, write a user callback function and bind it to the appropriate peripheral handle. Otherwise, you define the callback for ALL peripherals of that type, and other people's code will have conflicts/not work.
- Avoid blocking HAL functions where possible. If you do use a blocking HAL function, always specify a finite timeout.
Queues
- If you need to write to a queue,
extern
the queue handle. Be mindful that queues with many writers may fill up, and be prepared to handle that situation
- A queue should only ever have one reader. This is again a stylistic choice, but makes sense in our application since we don't have any parallel communication interfaces. If you have a situation where multiple things need to happen with the data from a queue, have a dedicated task, even if this means you need to read a queue then separate that data into multiple downstream queues.
- The task that reads from a queue is responsible for initializing it. This means write an intialization function in your source file, and call it in
main
prior to beginning the scheduler. You can't do this inside your task funcion because you can't guarantee your task will run and initialize your queue before another task tries to put data into it
Logging
- Use the logging methods described in
Tasks/log.h
to send logs to SD card.
logDebug()
calls will not run in production! Only for debugging.
- DO NOT USE STDIO PRINTF Instead, add
#include printf.h
to use this safer printf library.
- The methods are identical but with at the end (`snprintf("hi")
instead of
snprintf("hi")`).
- Use
printf_()
for printing debug msgs to UART4 (console). This method is ifdef'd out in production.
on-Target Integration Testing System (oTITS)
This module is an attempt at live integration testing. oTITS runs in a FreeRTOS task which periodically executes user-registered test functions and prints the results. Otits is meant to be a developer tool to help flag broken peripherals or tasks. It is somewhat intrusive and may disrupt timings slightly in order to test peripherals.
Quick-start:
- Enable/disable oTITS by adding/deleting the preprocessor define
TEST_MODE
(Project->Properties->C/C++ Build->Settings->Compiler->Preprocessor
).
- View sample usage of registering tests in
main.c
at test_defaultTaskPass()
.
Add tests:
- Add
#include "otits.h"
- Create a function with the signature
typedef Otits_Result_t Otits_Test_Function_t(void);
.
- The function should test something, then must return the results in a
Otits_Result_t
struct. The outcome
field is mandatory. The info
field can optionally be an empty string or any info that may be helpful while reading test results (ex, sensor data from the test).
- Register this function with
otitsRegister(Otits_Test_Function_t* testFunctionPtr, OtitsSource_e source, const char* name)
, where source
is the module and name
describes the test.
- All registration should be done before
otitInit()
is called in main.c
(ideally, do registration in each task's init method).
Read test results:
- Ensure
TEST_MODE
is defined (as described above).
- Open a serial terminal, connect to the board, run the program.
- Verify that the registration messages from
otitsInit()
contain no errors.
- A test is run every 99 ticks, cycling through all tests. Verify that a message is printed before each test, then a message containing the test results.
- Every time a full cycle of tests is run,
***TEST RESULTS***
shows a summary of all test stats. Verify that tests passed/failed as expected.
Unfortunately there is no convenient way to read the rapidly printing messages aside from stopping/starting and scrolling through the serial terminal.
Misc
- Do not use magic numbers. Any constant in your code should be delcared at the top of the respective: source file, if the constant does not need to be used elsewhere or header file if it does
- When you delcare a constant, either put the units (if applicable) in the name or in an inline comment with the definition
- If you find yourself delcaring a large number of local variables, put them in a struct. It's no OOP, but it keeps your code intent nice and neat, and makes it easier to change down the line.
- Do not change the build configurations. If you start getting dependency errors on canlib or vectonav libraries, the first thing I am going to ask is if you changed the build configuration. if you didn't, yell at me (Joe) and I will go deal with it
- Likewise, do not edit the .ioc file/regenerate the HAL code unless you really know what you are doing. Once I fix main this will not really be an issue.