[En-Nut-Discussion] Questions regarding UART implementation

Ulrich Prinz uprinz2 at netscape.net
Tue Oct 5 00:38:29 CEST 2010


Hi Thiago!

Am 02.10.2010 20:59, schrieb Thiago A. Corrêa:
> [...]
> I understand that we are not bound to how it's done on the PC, but
> that's definetely not how it works there :) You define the "packet"
> size with an ioctl, not the read call. It would make more sense
> indeed the way you propose, so you can handle different "packet"
> sizes
>
Yes, that's what I have in Mind. If you do packet oriented 
communication, in lots of cases you exactly know what's going out and 
what to expect. It the saves a lot of CPU time and memory if you write 
the software around the packets instead of searching for start / end 
markers, counters, length and copy from one buffer to another.
>>>
>> That sounds good. There definitely must be a ioctl option to check
>> if the return from read/write was cause of transfer errors or just
>> a timeout. I have to think about that again, but normally it looks
>> like this: rc = write( block, size, timeout); rc == size ->  good
>> transer rc<  size ->  transfer aborted cause of error or timeout rc
>> == 0 transfer timed out at all rc = -1 transfer aborted with error
>
> Then it would be changing the public API. The timeout could be done
> as in the PC world with an ioctl. I think that's how it's done today
> in Nut/OS as well, but not sure. I would like to keep read/write as
> standard C calls.

I didn't intend to change write and read. My intention was something else:
The basic change needed to do nice packet transfer is to change 
*StartTx(void) and *StartRx(void) to *StartTx(RINGBUF *rbf) and 
*StartRx(RINGBUF *rbf).

The functions can then check if the data is delivered in ringbuffer 
(rbf->rbf_cnt != 0) or as a packet (rbf->rbf_blkptr != NULL or 
rbf->rbf_blkcnt != 0)

With that small change, a no-ringbuffer option can be adopted to all 
platforms even with IRQ driven transfers. It makes sense as you save the 
RAM for the ringbuffer and the copy from ringbuffer to your buffers.

The second change then would be to upgrade platforms that support DMA or 
PDC to use these mechanisms as it again saves CPU time.
>
>>> True. Even if we implement the packet based API, we would have
>>> to make the serial buffer size configurable and make the call
>>> fail with some error code if it requests a packet larger than the
>>> buffer.
>>>
>> No. If you provide a buffer pointer to a buffer that is smaller
>> than the size you set for the expected bytes, it is your problem.
>> You cannot detect the size of a buffer behind a void* pointer. What
>> we need to do is to prevent the reception of more bytes than the
>> read was called for. These additional bytes need to be discarded.
>
> That would likely mess with the protocol. But the user could use
> flowcontrol with that feature to avoid losing data.

As Harald described me lately, the mechanism works like this:
You throw in bytes to the transmit buffer. Nothing happens until you do 
one of two things:
- You put some more bytes into the buffer that fills it above the high 
water mark what gets the transmit routine going.
- You issue a write(fb, 0, 0) what is the same as fflush(fb).

If you send more characters than the buffer can take, your thread will 
be blocked until the last character is at least in the buffer.

Reading is even more difficult if you like to do packet communication:
You request to read 128 characters. So you ask for
rc = read( fb, mybuffer, 128, 1000) what tells read to block your thread 
for 1 second and hope that the needed characters came in.
rc is now a number of characters that came in.
The timeout is done by a simple NutEventWait(usarthandle, timeout).
After that timeout it comes back to you, with something in between 0 or 
128 characters.
So what you like to get int that time can be less or far more than you 
expect (the rest is waiting in the ringbuffer). For a terminal 
connection this is pretty fine as you don't care how many characters are 
already sent to the other side and you are happy if you get the string 
the user typed when the software arrives at your scanf() or gets() function.

With packed communication you expect a special length packet in a 
special time. Nothing less, nothing more.
Especial if you need to do transceiver handling (DE/RE for RS485) it is 
important to switch after the last stop bit is out and not a us earlier :)

I thought about your idea about using ioctl(). I think that is pretty 
fine. You can add some flags and you'll be safe:
The rc = write(fb, packet, size, timeout) returns with the number of 
bytes really written. This can be counted in the interrupt routine or 
fetched from the DMA controller counter register.
If you see that rc != size you can request the detailed error 
information from ioctl(). For the 32 bit systems there should be enough 
bits left in the error flags to put in the DMA abort reasons.
>
> [...]
> If one set of functions for all USARTs is possible, it would be
> worth it. Even if it's only a few of them :) Perhaps it's because of
> the duplicate code, or the AVR32 gcc compiler is not good in code
> optimization, but looks like a simple "do nothing" program using
> Nut/OS and AVR32 takes about 30kb of flash. And it's still only 2
> uarts enabled in the compilation. For the A0 series, there should be
> 4.

I had some tests using twi, usart and some other peripherals with Nut/OS 
port to STM32F107 and it took 14k. Now I added a full CANopen stack, 
lots of console output and command interface, 3 usarts and structs for 
some external devices and I am around 38k.

But I had a hard trip though linker scripts and makefiles. May be I can 
check your scripts and Makefile in the next days.
>
> [...]
> Not sure how it works with STM32, but it looks like the DMA
> controller in AVR32 raises interrupts. This should fit well with how
> the usart handles the blocking of the "userspace" thread.
>
It is the same with the STM32.
You can choose between DMA-Half-Complete, DMA-Full-Complete, 
DMA-Last-Byte interrupts. In addition with USARTs you still can enable 
the TX-Complete interrupt, what is the best choice for packet transfer. 
Cause with this interrupt you are sure the bus is free.

So you set up a DMA transfer, enable the TXC Interrupt and do whatever 
you like then. After the last byte is out ( completely including STOP 
bit) you get an interrupt.

As packet handling means that you normally send a packet to receive 
information and then set the task sleeping until the information gets 
in, the StartTX() function can do a NutEventWait(packethandle...).
The TX-Complete Interrupt will do an NutEventPost(packethandle).

The StartTx() is called by write() what again is called from the thread 
that likes to write. So it is on the threads context and therefore a 
NutEventWait will pause the calling thread.


> [...]
> Ok. So, I guess for now let's go with private API's for DMA. Right
> now must implement other things that are priority for the projects
> I'm working with, but I will try to at least follow the same func.
> signature you use.
>
Fine. That's why I thought that it makes more sense to split that work 
into two parts. First I will modify usart.h to send the rbf pointer to 
all transmission related functions. That can be done for all 
architectures without breaking anything.

The second part then would be to hand over the dcb instead of the rbf. 
That would enable to have smaller rbfs for packet mode and to use one 
single software for any amount of USARTs in one chip.
> [...]
> It's actually fairly common :) You could make macros/inline functions
> to make it easier to understand: is_ringbuffer() or
> is_block_transfer() or something like that. Reduces the magic number
> comparisons in the code.
>
That isn't the problem. But I already smashed Michaels brain by a short 
excursus into DEVICE-BUS-NODE chains. I don't want to send you offline 
too :)

No, just a short one:
Normally you program a device driver that uses a bus to communicate with 
a node.
So my temperature sensor is a node on TWI bus and the LM75 driver talks 
via the TWI bus with the registerd LM75 node at address xy.
The EEPROM driver uses the same TWI bus but registered a node 0x50, the 
EEPROM.

Now, things get funny, if you look at two things:
The Terminal device registered as stdout, registers a OLED driver that 
connects to the SPI node (at /CS1) via the SPI bus.
Here the gag is that there are drivers chained. But it is possible as 
for SPI this DEVICE-BUS-NODE concept is established in Nut/OS.

With USARTs it is not and with I2C it isn't too.
USARTs are registered as devices, what is wrong as they are busses.
That's why someone could get dizzy while studying Nut/OS.
The terminal is the device that uses the USART-bus to talk to a node 
what can be the PC at the other end.

If you implement it that way, you can implement one thread that talks to 
your small sensor devices on the other end of a RS485 bus. If you have 
20 of them on one bus, you fire this single thread 20 times and pass the 
sensor address as a parameter. It the registers the nodes at the USART 
bus and happy you are.
With the actual USART implementation you need to write a dispatcher that 
handles all traffic and sends it to the threads.

For that reason I already rewrote the STM implementation of the TWI bus.
Let's see how it runs in the next few weeks.
>
> Well, the debug port is actually is a different driver, without
> interrupts. I was thinking about sharing ring buffer handling or
> high watermark/low watermark handling.
>
I did it the other way round. With the nutconf option you just declare 
one of the uarts to be the debug port. The rest is identical. But you 
have to have interrupt going. If you adapt the driver with at least the 
ringbuffer pointer presented to all functions, you can adopt polling 
driver as  an option for all usarts.
>
> I tried devnut_cortexm3 at work and someother I can't remember atm.
> Didn't seem to have problems with it. I will try monday on
> devnut_m2n
>
The other cortex branches where just interim ones. I needed them for 
cross merging of some ports I got. I'll delete them in a few days. As 
soon the stm port is running fine and Michael did some work on his 
Luminary port, I think about switching it to the cortex or stm32 branch. 
But the best would be we could merge it to the trunk.
>
>>>>
>>>> By the way, Option 2 is what I did for TWI cause STM32 has two
>>>> interfaces and 4 interrupt providers ( two per interface) that
>>>> call the same code existing only once. Old Tw*() functions are
>>>> #defined to the stm32 specific functions. Works fine here :)
>>>>
>>>
>>> Yesterday I was thinking about a platform independent TWI. So we
>>> could have platform independent drivers to access EEPROMs and
>>> Atmel QTouch chips.
>>>
>> ... Now I am sad... I already did that.
>
> That's good news :) Is it in trunk or just in one of the branches?
>
The EEPROM driver is in the trunk. The STM version is a bit more 
sophisticated. Doesn't really need much mor flash or ram, but comes 
around a problem with large cross traffic ( multiple devices sharing TWI 
bus) and with those small Microchip EEPROMs where the lower three bits 
of the chip address is misused as page select for the internal address. 
I hate Microchip, they have no idea of what I2C is.
And they already cost us a lot of money with their whatsoever bus.

> [...]
>> The gpio.h then only includes an architecture specific xxx_gpio.h
>
> I'm starting to take a look at what you wrote in the wiki. I'm going
> to start another thread on that subject :)
>
Yes, we should keep that off from this thread :) But feel free to add 
AVR32 related things into the wiki and add corrections and opinions.

[...]

So, best regards, good nite and CU!

Ulrich



More information about the En-Nut-Discussion mailing list