Source: https://openrcforums.com/forum/viewtopic.php?f=124&t=6712&p=93104
Timestamp: 2019-04-24 11:47:14+00:00

Document:
I have added board driver for fs-t6 to my libk project.
* display driver added. Have it working together with vt100 driver and serial interface to make it really simple to print text to the display.
* input channels: analog working, keys working, switches working.
* PPM output: not yet done.
LibK is a work in progress library for building firmware applications that run directly on bare metal hardware. Currently tested architectures include arm-stm32f103, arm-stm32f100 and avr-atmega328p.
I see big benefits: reuse device specific code for multiple projects, make application code more portable across different hardware (opentx/9x etc).
I can see some potential problems: code bloat (abstractions), performance (least common denominator, layers), polluting generic libk with application specific services ("read sticks").
Earlier the better (less work later) but one would need to port all the services currently working in art6 into libk to make it functionally compatible. However, at this point we are trying to get the art6 "off the ground" first.
I can see that with several abstraction layers one could construct a library that would help portability. I can see that higher abstraction layers, e.g. such as storing/retrieving settings could relay on lower layers such as block driver (flat memory concept) then chip driver (EEPROM/FLASH) then bus (i2c/spi) then actual io (hardware or bitbanged software impl). And one can choose to implement at particular layer to suit convenience, performance or other needs. OK.
Abstracting graphics display (different size, colors?). Menu system? PPM? It feels some abstraction layers are strongly part of application type (such as "transmitter fw") and hence perhaps belong to the application, should it choose to be portable. Unless you mean to focus libk on transmitter fw?
I do not have much time at the moment but let's continue. Perhaps Richard can contribute a thought as he put nice solid foundation into art6.
Abstracting graphics display (different size, colors?). Menu system? PPM? It feels some abstraction layers are strongly part of application type (such as "transmitter fw") and hence perhaps belong to the application, should it choose to be portable.
No these don't need abstractions. The scope of libk is to provide abstraction for framebuffer only. Then any graphics library can use the "canvas" to draw stuff on any supported display. And yes, graphics library is part of the application code - as well as things like flight control algorithms etc.
PPM however would be an platform layer function, just like PWM (because it is greatly dependent on the hardware - in this case timers). It would be reasonable to have configurable ppm as part of arch/soc layer that the board code can use to set ppm signal. The board code is part of libk however, but it's job is to expose a simple interface to the board that the application code runs on. For instance on fs-t6 radio it could be a call that looks like: fst6_write_ppm(chan1, chan2, chan3, chan4, chan5, chan6). That would be the only call that application would need to instruct fst6 board to send channel values to the rc model. The rest of the code of actually generating the ppm signal would be part of the library with parts of it being very architecture specific.
State of I2C: on avr i2c is all done and interrupt based. On stm32, i2c is currently very simple procedural approach that works, but is far from ideal. Ideally one would implement i2c using dma and with the same 3 calls: i2c_start_write, i2c_start_read and i2c_stop. I will add it into https://github.com/mkschreder/martink/b ... tm32/twi.c once I get dma i2c working.
The abstractions are all very thin. I could even have chosen to wrap them all into macros - in fact a lot of the lowest level stuff on avr is all macros. But I have moved away from macros for things like timestamp_expired() calls because macros made the overall size of executable too large (-Os flags have no effect on macros). The main abstraction is the layer that provides interfaces. I have come to conclusion that this layer is absolutely necessary if one wants to have things like external devices working seamlessly regardless of which number i2c interface they are connected to and regardless of how the connection is made. For example, it is possible to use gpio abstraction to treat an i2c gpio expander as though it was a normal on chip gpio. This allows to use many devices that may be controllable through normal gpio over i2c gpio (for instance LCD displays that connect over pcf i2c gpio expander). So I use some abstractions - but very few.
The layers of abstraction are similar to what desktop os uses such as linux, but we remove A LOT of the stuff that is completely unnecessary on small processors like the ones in many of the boards we are working on. The idea is that instead of following the "OS" model which uses threads and other multitasking concepts, we remove all of the uncertainties associated with multitasking and instead focus on reusable abstractions. OS model is really not perfect and even frameworks that run on desktops, like nodejs, are moving away from the threaded model completely. Without threads, abstractions are thinner, code more simple and concurrency issues gone completely. Without threads and without multi-user environment one does not need to have all that extra code that handles these things. Libk uses instead a very tight main loop, interrupt based data transfers and systicks counter for running tasks at intervals. But everything is all single thread and no application code is ever interrupted by other application code.
Performance: here is how it works: if speed is absolutely necessary one can use arch specific calls such as "gpio_set() or gpio_clear()" directly. All these calls are present for the application to use (on avr these calls are macro based so speed is maximal). However, if one needs to use a device that is usable on both arm, avr, or any other board - then one would naturally implement an abstraction anyway. So sacrificing a little of performance for possibility of writing a more portable firmware is something one is usually willing to do. Also, code bloat is greatly reduced when code is factored into smaller parts that are responsible for specific tasks.
Perhaps Richard can contribute a thought as he put nice solid foundation into art6.
The best point is when one wants to start using some of the existing device drivers. There are probably many small issues that will get fixed along the way as support for specific boards is added. Yet it took me only a couple of days to add support for fs-t6 transmitter board. I did not need to spend a long time getting the basics to work - all the basics were already there. I just added an LCD driver, fixed linker scripts to support stm32f100 chip and made sure it all compiled and run, then added board support in "boards/rc/flysky-t6-tx.c". The result is I can easily print text on the lcd, read the sticks, use serial port, save settings to the on board eeprom etc. I can even run other programs that I originally developed for atmega328 arduino. Now I would only need to get ppm implemented find a suitable gui library. Had I already had a completed tx firmware that only needed an LCD interface to draw it's gui on a display then I could have easily compiled it and it would work out of the box on fs-t6.
Ok so the target for libk is the rc transmitter. Initially I though libk is aiming into a general MCU audience. The libk would provide a "hardware isolation" uppon which opentx etc could build a hw independent code?
if one wants to have things like external devices working seamlessly regardless of which number i2c interface they are connected to and regardless of how the connection is made.
BTW, the eeprom code in art6 works with i2d hardware, dma and interrupts. It's specific to the EEPROM chip and STM32 i2c/dma/irq but you could leverage it as well. Although this is the least important part of the code that takes advantage of raw hardware.
The art6 code aims to do as much as possible by stitching the stm32f1xx hardware devices, through DMAs and IRQs. E.g. the ADC acquisition is triggered by hardware timer and then deposited through DMA into memory, then at last the ISR is called where the ADC reads are converted to "sticks values" (well at least it should be as the moment it's a main thread that updates it). Mixer code and PPM are also in hw timer ISRs. Ideally only the GUI is running on main thread (the EEPROM r/w, called from main thread, are also irq/dma albeit since they are a blocking calls to the user there is no clear advantage of this...other then at some point we can save power by sleeping during EEPROM writes/reads while right now we busy-wait for dma/i2c to finish). The overall intention is to sleep and wake up only for irqs and for GUI updates.
Are you planning to address such capabilities in some way in libk?
Statically linked call is specific to the board because it uses board specific devices and GPIO pins to generate the ppm. Interfaces are optional.
I know why you did it that way. Because sharing an i2c device between multiple parts of code while the device is processing a transfer in the background is tricky at best. Requires a queue of requests if one wants to avoid waits altogether - which means higher memory usage. And so it is best to wait for the transfer to complete. It doesn't matter anyway since eeprom is not written every frame. So sometimes it may be much more efficient to wait than to use automatic transfer. In fact, it takes me only about 1ms to both construct and send the whole display buffer to the display - including pin toggling etc. and that is without DMA even - 1000fps. So one has to start optimising when the main loop no longer can process the tasks in due time. There is much more time available than one would imagine. The worst thing to watch out for are blocking delays. Everything that uses blocking delays is on todo list to be refactored into asynchronous code.
I recently implemented ADC using dma and that works nicely - but no timer is needed. Just set ADC into automatic mode and have it automatically trigger DMA when conversion is completed. The result is an array of values stored in memory which the main code retrieves from the adc subsystem. Since many subsystems are interdependent, I try to work out ways to use as much hardware support as possible, while still providing a rich array of services for the application layer. My PPM is also generated in timer ISR. And input capture and pwm generation is all in the timer ISR as well.
I recently implemented ADC using dma and that works nicely - but no timer is needed. Just set ADC into automatic mode and have it automatically trigger DMA when conversion is completed.
The timer-triggered conversion is due to a low sampling rate required for sticks and hence allows to save power. With auto triggering you'll get the highest possible rates and power consumption. But I'd think this could be hidden in board-specific code anyway (fst6 board purpose is rather fixed to be a transmitter)? The same goes for rc ppm pulse train generation.
What is your thinking about abstracting ISRs (say pin triggered or timer triggered)?
BTW, Is your code capable to write fst6's EEPROM in quantities smaller than the page? I have problem with this.
I was mistaken thinking libk is specifically & only for transmitters.
No, not at all specifically for transmitters. But board files are specific for each board layout and on-board hardware. Board files take care of initialising all on-board devices connected to the soc interfaces. One could think of the board files to be responsible for the board layout as the arch files are responsible for the SoC layout. The board files use the power of underlying interfaces to easily construct valid configuration for a specific board.
This is what I do in my copter project - basically when I include board specific code, I have defines that translate into fc_init fc_whatever.. It is nice to do it this way. Before I had fc defines directly in the fc board headers, but moved them out just recently to keep the headers cleaner.
One thing that I'm completely against though is #ifdefs scattered around the code. This is a practice used in many projects and it is absolutely horrible. I still can't compile multiwii without arduino (using just avr-gcc) because some unknown ifdef does that it doesn't doesn't work (it compiles fine but does not work). I haven't bothered with it after that - but the lesson is that #ifdefs can make code break in mysterious ways because the programmer has no obvious ways to know which ifdefs are active and which ones aren't. And when none of the ifdefs have #else statements that would trigger if no valid option is selected, it becomes a maintenance nightmare. So #ifdefs should only be used in specific places to include specific header files or to provide an interface based on which headers are being used (like above). And they should never have invalid #ifdef paths that will compile but not work (should use #else and #warning in that case).
Got PPM working today for FS-T6 TX.
I don't know how much flash space you have on the FS-T6, but you might be interested is saving some flash space at some point.
#define gpio_configure(pin, fun) . . .
followed by quite a bit of code. Every time you use gpio_configure, you get another copy of the code compiled in. You might consider using this as a function rather than a #define.
that allows the full configuration of I/O pins. The first parameter is a mask of the pins in a port to configure and the second is the definition. If you have several pins to configure in the same way on a port(e.g. all outputs or all allocated to a specific peripheral) you can configure them all with a single call.
That's for AVR. For STM32 there is void gpio_configure(gpio_pin_t p, uint16_t flags).
Avr code is very much macro based for maximum speed. However some macros that are getting bigger will be converted to static inline / normal methods as part of optimization stage.. The avr macro layer is designed for maximum speed though.
Speed on the AVR doesn't really matter for things that are likely to only be used once at startup. Having had to shrink er9x many times to fit on the M64, space is far more important there.
You may still like to look at my code for the STM, it includes allocating a pin to peripheral use.
Mike, you worry about size yet you still are not garbage collecting unused code and data?
It's the FrSky version that is in most need of flash space saving. Unfortunately, using those options for that version doesn't save any flash space. So there are probably a few functions in the standard version that are only needed for the FrSky version.
Mike, you mentioned stm32 code above. In the repo I saw only avr code. Where was that stm32 code?
You need the ersky9x sources from here: http://code.google.com/p/ersky9x/source ... runk%2Fsrc. configure_pins() is in logicio.cpp.
How come you are not using ST peripheral lib?
Is there an easy way to rip out gui code and make it use a set of specific methods for drawing everything? I'm thinking if gui code can be completely self contained and only focus on the gui it would be trivial to then render it using libk lcd interface on any lcd (I can add necessary graphics functions). It would be interesting to see if any space can be saved as well - but I have positive expectaions. I think it is possible to make it much more compact. For example you have a lot of custom code for printing hex strings etc. For that, stock libc function can be used. So all of that code can be completely removed. Also timestamp_*() set of functions can be used for all the stuff where you need some action to run at a time interval. I see you are using timer variables. There are probably other areas that can be made much more straight-forward, but I haven't looked.
Does this codebase work on flysky-fst6? I was not able to compile it yet with the included makefile.. you should include coocox.h in the source tree as well. Would be nice if it would build with just make and arm gcc.
I would love to test it on flysky board. Do you think you could separate out all processor specific code from application logic? I could provide all the necessary hardware support if only the fw logic is self contained and uses a documented set of functions to access all necessary hardware data. Is it something you are willing to do?
When I started porting er9x to ARM It was for an Atmel 3S2 processor, later a 3S4.
I wrote all the peripheral drivers for that. When the Taranis came along I therefore implemented drivers to match the calls I already had. I find the ST peripheral lib long and involved from a user view. I much prefer my configure_pins() for example.
The code is similar for er9x on the AVR processors as well as ARM processors, I don't want them to be different. I'm actually trying to merge the sources back together. I certainly don't want anything like printf/sprintf on the AVR, they take up too much flash space and run too slowly. We have lcd.cpp that contains all the display application routines, virtually identical for AVR and ARM. Since I don't think any significant code from libc is used, particularly on the AVR, I don't think using it will save any space on the AVR (see: http://code.google.com/p/er9x/source/br ... runk%2Fsrc).
coocox.h is there in the trunk/src on the googlecode repository!
When making you need to specify some options to get build, make by itself is not sufficient.
I'm slowly working on removing processor specifc code to it's own source files, but I don't have enough time to make this a priority.

References: art6
 art6
 art6
 art6
 art6
 art6