Pages: [1]   Go Down
Author Topic: What's a good way to be hardware independent?  (Read 679 times)
0 Members and 1 Guest are viewing this topic.
Anchorage, AK
Offline Offline
Edison Member
*
Karma: 42
Posts: 1176
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I'm looking for some advice from better codesmiths than I -- or at least a healthy debate on merits of different approaches.

I'm not much of a C++ guy (just haven't wrapped my mind around all the syntax of classes yet), so I haven't written any Arduino libs.  However, I have a few things that are much larger than a sketch should be, so I want to take advantage of code re-use.  Also, I'm philosophically drawn to pure, vanilla AVR libc development, which lends itself to C libraries.  I'd especially like to develop libs that can be used in AVR Studio and in Arduino projects..  The Arduino headers offer things that make this fairly convenient, such as returning ports for Arduino pin numbers and whatnot, and I can selectively include Arduino compatibility by checking for defined flags, so that's great.  However, I'm stuck on how best to allow the user (me, for now, but especially if I later release any code) to customize the layout of the hardware stuff.

For example, I'm writing an SD Card library that can be used in AVR Studio C projects, so the Arduino SPI class is out.

SIDEBAR:  Before anyone jumps on this, bear in mind two things:  1) I'm aware I can create a C++ project, incorporate Arduino libraries, and otherwise pretend I'm still coding C, but this is a thought exercise so I'm not going to take the easy way out; and 2) Yes I know there are fully-baked SD libs already, but sometimes I enjoy reinventing the wheel just to learn how one is made.  I now appreciate LBA and the difference between half a dozen FAT signatures where before they were just ethereal concepts.

Given this example, what is a well-established way of allowing the user to define the pin they've chosen for CS?  (Or even MOSI/MISO/SCLK if using soft-SPI?)  In Arduino code, it would look something like this:

Code:
// Use D8 for CS
class.init(D8);

But that assumes the Arduino IDE method of blending all the source files into one big .cpp and compiling it with all the proper includes baked in for you.  If you take this example into AVR Studio, with independent .c files, you break the "globalness" of all the #defines.  Ideally, I'd like something like this:

Code:
/* MyProject.c */
#define PIN_CS   PORTB0

#include "somelib.h"

// Use PORTB pin 0 for CS
somelib_init(PIN_CS);

Obviously, this doesn't work.  The constants like PORTB are dereferenced to the value at their defined memory location -- i.e., they return a value instead of representing an address (well, unless you define _SFR_ASM_COMPAT_ in the libc headers, breaking the expected behavior for every other module in the process)  And there's no way to specify a pin, only the whole port.  This makes sense of course, as the return value you get back is a bitmap representing 8 pins' states.

I thought about just using defines:

Code:
#define PORT_CS  PORTB
#define PIN_CS  PB0

#include somelib.h

somelib_init();

But of course that won't work either.  The library wouldn't have access to definitions in the main code file, since by definition they're local to that file.

The only workaround I see is to either 1) have the end-user modify the library headers to configure the pin defines (practically requiring local copies for each project), or 2) #include config.h and have the user supply their own header with their definitions (with pretty much the exact same problem).  Both options seem ugly.  I would assume someone has thought of a more elegant approach.

Sorry for the enormous post.
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 474
Posts: 18696
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Whether or not you use the Arduino IDE is not really the point here.

Changing defines before including an include file is fraught with peril. Other CPP files that include that file won't have your magic defines in front of them.

The standard method is to pass things like pin numbers to the constructor, or some initialization function (like "begin").

I don't see what the objection is to that.

Logged

Anchorage, AK
Offline Offline
Edison Member
*
Karma: 42
Posts: 1176
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Pin numbers only have relevance within the Arduino environment -- whether you use the IDE or not is, as you say, of no concern.  However, if your goal is to support as many platforms as possible (both Arduino and general AVR), things like "D0" cannot be depended on.  That's one issue.

The second is this:  Imagine you're trying to write a driver for a new LCD chipset that uses an 8-bit data bus.  Do you really want to pass (D0, D1, D2, D3, D4, D5, D6, D7) to the init function?  You can gain a *ton* of efficiency by using atomic 8-bit operations on a whole PORT instead.  In some cases I've seen, when a developer writes a library like this, they create it with the intention of running on an ATmega 328, and tell you which pins will be consumed.  If you hope to use that library on a 1284, well... prepare to track down all the hardware-specific parts and re-write them yourself.

I'm looking for a way to support any current or future MCU (within reason) without having to republish the library with chip-specific layouts when they change, or have the user go and modify library sources because they want to use PORTD instead of B.  Unfortunately, I'm just not aware of any good method to allow a library user to specify a port or pin in a portable way, since all the standard defines are intended to ACCESS the port, not provide a way to pass its hardware address through function calls.  I'm hoping this is achievable in some way I haven't thought of yet, though I realize it just may not be.

Make sense now?
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 474
Posts: 18696
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

It's going to be difficult to achieve that. For one thing, on the Uno, only one port (D) gives access to 8 bits and even then two are shared with the serial ports. And on other processor what if the user happens to want to use pins that aren't in a single port?

The more hardware-independent approach (using pin numbers rather than hardware ports) is what the IDE currently supports, for good reason.

Quote
You can gain a *ton* of efficiency by using atomic 8-bit operations on a whole PORT instead.

It will be faster. I don't know about quantifying it like that. A bit depends how often you write to the LCD. A panel showing the room temperature, for example, won't matter if it updates a few microseconds faster.

Quote
I'm looking for a way to support any current or future MCU (within reason) without having to republish the library with chip-specific layouts when they change, or have the user go and modify library sources because they want to use PORTD instead of B.

The current system attempts to do that, with its table mapping ports (and pins within ports) in a platform-independent way. So that digitalWrite (3, HIGH) works on lots of different processors.

I'm not sure that what you are proposing (rejecting the IDE and then substituting something that achieves the same end-result) is going to be worth the effort.
Logged

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

First of all, you should have reasonable expectations as to what portability can do for you. You will never be in a position to take the source code to a new mcu, compile it and expect it to work.

However, if you code to portability, your task of porting a code to a different hardware can be greatly simplified. For example, let's say that your code needs to send a string via i2c. You can simply code the hardware spi in your user code, or you can call a routine called i2c_write() to send a byte. You can then link in different i2c libraries (hardware or software, avr or stm32, etc.) And you know with confidence that your code will work.

The key to write portable code is really to modulize your code so you limit your hardware touchpoints.
Logged

Anchorage, AK
Offline Offline
Edison Member
*
Karma: 42
Posts: 1176
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

It's going to be difficult to achieve that. For one thing, on the Uno, only one port (D) gives access to 8 bits and even then two are shared with the serial ports. And on other processor what if the user happens to want to use pins that aren't in a single port?

This is exactly the type of thing I'm concerned about.  Depending on the application, giving up the serial port may be of no consequence, while SPI is mandatory.  Or vice versa.  The details are of course only an example.  This isn't an Uno problem, or a serial problem, or anything like that.  It's about not enforcing restrictions based on my current priorities.

The more hardware-independent approach (using pin numbers rather than hardware ports) is what the IDE currently supports, for good reason.

And there's considerable cost to that, which may or may not be forgivable in exchange for the convenience.  Again, depending on the application. :-)

Quote
You can gain a *ton* of efficiency by using atomic 8-bit operations on a whole PORT instead.

It will be faster. I don't know about quantifying it like that. A bit depends how often you write to the LCD. A panel showing the room temperature, for example, won't matter if it updates a few microseconds faster.

Well, yeah, naturally, but you're getting lost in the particulars of an example.  For the display on a digital clock, you're absolutely right.  Now what about the audio processing unit from a Super NES?  That's four address bits, 8 data bits, and the usual RSET, RD, WR, CS pins.  Exactly two 8-bit ports.  If you want to stream audio to it, using a dozen or more calls to digitalWrite for every byte of data is out of the question since digitalWrite will have to map the pin to a physical port, ensure it's a sane request and/or that the port is in the proper state, do a read-modify-write, then repeat the whole process for the very next bit in the same byte.  That's hugely inefficient, at well over 10 times the overhead.  This would apply to any external peripheral on a parallel bus that is subject to performance constraints.  By that point, if you had to choose between parallel RAM and SPI-based RAM for example, SPI could very well be faster.

I'm not sure that what you are proposing (rejecting the IDE and then substituting something that achieves the same end-result) is going to be worth the effort.

Huh?  No, that's not at all what I'm proposing.  I'm really not proposing anything, actually.  I'm asking if there's a buried macro or trick that anyone knows of that can allow passing (full or partial) PORTS as parameters.  If you look at the avr-libc headers, this is what happens with raw PIN access (example taken from the ATtiny 2313):

Code:
  // io2313.h, line 77
  #define PINB _SFR_IO8(0x16)
 
  // sfr_defs.h, line 179
  #define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
  // sfr_defs.h, line 128
  #define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

So, 0x16 is the address.  To use it, the macros just cast a pointer to that address, then resolve it as an anonymous variable.  The Offset bit is necessary for certain instructions that access addresses as either raw memory or as I/O.  All I need is a way to get that address, and then a macro to Do The Right Thing with it.  AFAICT, there isn't an existing method built for that, but I thought I'd ask...  I guess for now, dhenry is about spot on.
Logged

nr Bundaberg, Australia
Offline Offline
Tesla Member
***
Karma: 126
Posts: 8474
Scattered showers my arse -- Noah, 2348BC.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

I'm writing a HAL for LPCs at present, I have a flat model of "logical" pins from 0 to N (where N varies depending on the chip being used), it has atomic access to any number of pins up to 32 by creating "pin groups", simple and adjustable debouncing on all pins with almost no CPU overhead, even "virtual pins" where the call is trapped and a handler can get data from a network or wherever  etc.

The analogRead() func just accepts a pin # like all the other funcs, only you get an error if the pin is not connected to the ADC, but soon the ADC reading will happen in the background and analogRead() will just return the value from an array, that makes it independent of the hardware but of course you still need the low-level background task.

Above the "logical pin" level there is what I call the "application pin", which is really just a lookup mapping from one to the other.

I've not really looked at how this would all port to a different architecture because I was really just interested in having an Arduino-compatible environment on my LPCs, but it's pretty modular so it might.


______
Rob
« Last Edit: October 25, 2012, 08:57:02 pm by Graynomad » Logged

Rob Gray aka the GRAYnomad www.robgray.com

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 474
Posts: 18696
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
I'm asking if there's a buried macro or trick that anyone knows of that can allow passing (full or partial) PORTS as parameters.

Try looking up digitalwritefast:

 http://code.google.com/p/digitalwritefast/

That uses a compile-time trick to map, where possible, digital writes of constants to a direct port, removing the runtime lookup.

Logged

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
All I need is a way to get that address...

You can compile the addresses off line, put it in a device-specific .h file and write your macros  assuming availability of such header file and built your applications off these macros. When you port your code to a different chip, you can include the device-specific .h file with the addresses coded in, and  your code will work. The same principle that I talked about earlier.

This is no different from the arduino approach, in that it is a trade between convenience / portability vs. speed / performance, especially if you want to resolve the pins at run time vs. compile time.

As to your read-modify-write comment, that is largely alleviated by the "slow" IO necessitated by the use of such an approach. As such, the PINx registers are ignored. If you are speed conscious, and you are OK with compile time resolution of pins, you may need to re-instate the PINx registers.

On older ARM chips, they have bit-banding that is quite helpful in situations like this.
Logged

Anchorage, AK
Offline Offline
Edison Member
*
Karma: 42
Posts: 1176
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Guys, thanks for the feedback on this.  Rob, I'm looking forward to seeing what you come up with.  Being that I am exclusively into the AVR8 platform right now, my present concern with "portability" is between Arduino and vanilla AVR C environments, although I suspect I will eventually branch out into ARM when I find myself regularly banging my head against processing and memory limitations.  For now, if I can come up with a way to compile code on anything from an ATtiny to an ATmega1284p with fairly minimal modifications, I'll be happy.

I did find the _sfr_addr() macro, which I think gives me the address I'm looking for, I just have to find out if it's universally safe to perform IO with _sfr_io8(addr) or if there are differences within the product line that would break that approach.  Finally, I would want to have wrappers to resolve more friendly nomenclature, like "PINB" on AVR, and e.g. "D12" if _ARDUINO_ is defined.  I would prefer this all be done by the preprocessor, so the compiled code is as close to hardware as it can get.  Compiler optimizations might also help resolve things like pin offsets, turning "PINB & (1 << PB5)" into "*(uint_8t *0x18) & (1 << 5)" into the cheapest assembly instructions that would achieve that I/O request.  Finally, I noticed there seems to be a trend in the register addresses where the order is PINx, DDRx, PORTx on 1-byte boundaries.  Again, not sure if that's "common" or "guaranteed" yet.

I want to avoid too many device-specific headers, since that duplicates a lot of work, requires that I maintain those duplicates, and makes human error exceedingly likely.  If it came down to that, I think I'd rather just have the user search-and-replace the PIN/PORT/DDR registers with their preferred ones.
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 474
Posts: 18696
Lua rocks!
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
Compiler optimizations might also help resolve things like pin offsets, turning "PINB & (1 << PB5)" into "*(uint_8t *0x18) & (1 << 5)" into the cheapest assembly instructions that would achieve that I/O request.

The compiler is very good at that. For example:

Code:
void setup ()
  {
  DDRD |= 1 << 4;  // pinMode (4, OUTPUT);
  PORTD |= 1 << 4;  // digitalWrite (4, HIGH); 
  }  // end of setup
 
void loop () {}

Generates:

Code:
void setup ()
  {
  DDRD |= 1 << 4;  // pinMode (4, OUTPUT);
  a6: 54 9a        sbi 0x0a, 4 ; 10
  PORTD |= 1 << 4;  // digitalWrite (4, HIGH); 
  a8: 5c 9a        sbi 0x0b, 4 ; 11
  }  // end of setup
  aa: 08 95        ret

A "shift" and an "or" has been optimized into the "SBI" assembler instruction. Can't get more efficient than that.
Logged

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I was thinking about it yesterday and here is something that is quite close to the arduino approach, but doesn't suffer its performance penalty:

Code:
#define VAR_SET(var, id, pin) var ## id |= _BV(pin)
#define VAR_CLR(var, id, pin) var ## id &=~_BV(pin)
#define VAR_TOG(var, id, pin) var ## id ^= _BV(pin)
#define VAR_GET(var, id, pin) ((var ## id) & _BV(pin))

#define pinOutput(pin) VAR_SET(DDR, pin)
#define pinInput(pin) VAR_CLR(DDR, pin)

#define pinSet(pin) VAR_SET(PORT, pin)
#define pinClr(pin) VAR_CLR(PORT, pin)
#define pinTog(pin) VAR_TOG(PORT, pin)

You would define a pin, like
Code:
#define HC595_SCK B, 0 //hc595 on portb.0

and use it later:

Code:
  pinOutput(HC595_SCK); //hc595_sck as output
  ...
  do {
    pinClr(HC595_SCK); //clear hc595_sck
    ...
    pinSet(HC595_SCK); //set hc595_sck
  }

If you have reassigned HC595_SCK to PORTC.6, you just need to redefine it:

Code:
#define HC595_SCK C, 6 //sck now on portc.6

and recompile.

Something like this is very portable as it does not rely on bit fields, and present no performance penalty or re-entrance issues.

Logged

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

On a 1MIPS avr, this approach can flip a pin at 100Khz. So at 16Mhz, 1:1 prescaler, it should be able to do 1.6Mhz pin flipping.
Logged

Offline Offline
Edison Member
*
Karma: 116
Posts: 2205
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Here is another take on the same thing.

gcc-avr supports bit fields. This allows value to be directly assigned to a bit.

Code:
typedef struct {
unsigned char B0: 1;
unsigned char B1: 1;
unsigned char B2: 1;
unsigned char B3: 1;
unsigned char B4: 1;
unsigned char B5: 1;
unsigned char B6: 1;
unsigned char B7: 1;
} BIT8_T;

//extend bit fields
#define PORTBbits (*(volatile BIT8_T *)&PORTB)
#define DDRBbits (*(volatile BIT8_T *)&DDRB)

//helper functions. don't call directly
#define _DDR(id, pin) DDR ## id ## bits.B ## pin
#define _PORT(id, pin) PORT ## id ## bits.B ## pin

//port functions
#define DDR(pin) _DDR(pin)
#define PORT(pin) _PORT(pin)

PORTBbits and DDRBbits are bit fields of their whole byte countparts - you can expand them easily to cover other registers, like  PINx.

DDR() and PORT() functions are actually what we want. _DDR() and _PORT() are there to defeat the compiler so don't use them.

If you define a pin like this:

Code:
#define HC595_SCK B, 0 //hc595_sck on portb.0

You can later directly assign values to it, like this:

Code:
#define HC595_SCK B, 0 //hc595_sck on portb.0

  DDR(HC595_SCK) = 1; //hc595_sck as output

  do {
    PORT(HC595_SCK) = 0; //clear sck
    ...
    PORT(HC595_SCK) = 1; //set sck
  }

...
PORT(HC595_SCK) ^= 1; //toggle hc595_sck

To some, this may be more intuitive. But it is slightly slower, due to the bit fields.
Logged

Pages: [1]   Go Up
Jump to: