Roll mt own Serial3 function in sketch for one way TX communication to a Serial LCD

Status
Not open for further replies.

BLMinTenn

Well-known member
Gents,
I need some expert advice. I want to breakout the needed basic functions in the Serial3 library and put them directly into a sketch that will eventually transmit one way to a serial LCD. I have some code below... which is what I dream of my end result being. FYI, I have not tested this on a compiler, so there may be some slight errors.


Code:
#include "kinetis.h"
#include "core_pins.h"

#define C2_ENABLE		UART_C2_TE | UART_C2_RE | UART_C2_RIE
#define C2_TX_ACTIVE		C2_ENABLE | UART_C2_TIE
#define C2_TX_COMPLETING	C2_ENABLE | UART_C2_TCIE
#define C2_TX_INACTIVE		C2_ENABLE

static volatile uint8_t tx_buff_head = 0;
static volatile uint8_t tx_buff_tail = 0;
static volatile uint8_t tx_buff[40];
static volatile uint8_t transmitting = 0;
static volatile uint8_t *transmit_pin=NULL;
#define transmit_assert()   *transmit_pin = 1
#define transmit_deassert() *transmit_pin = 0

uint8_t my_array[8] = {0xfe, 0x94, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35};

//8N1 = 0x00
//8N2 - 0x04
LCD_begin(uint16_t baud, uint8_t format) {
//format section--------------------  
      uint8_t c;

        c = UART0_C1;
        c = (c & ~0x13) | (format & 0x03);      // configure parity
        if (format & 0x04) c |= 0x10;           // 9 bits (might include parity)
        UART0_C1 = c;
        if ((format & 0x0F) == 0x04) UART0_C3 |= 0x40; // 8N2 is 9 bit with 9th bit always 1
        c = UART0_S2 & ~0x10;
        if (format & 0x10) c |= 0x10;           // rx invert
        UART0_S2 = c;
        c = UART0_C3 & ~0x10;
        if (format & 0x20) c |= 0x10;           // tx invert
        UART0_C3 = c;
//baud & setup section--------------
    SIM_SCGC4 |= SIM_SCGC4_UART2;	// turn on clock, TODO: use bitband
	tx_buff_head = 0;
	tx_buff_tail = 0;
	transmitting = 0;
    CORE_PIN7_CONFIG = PORT_PCR_PE | PORT_PCR_PS | PORT_PCR_PFE | PORT_PCR_MUX(3);
	CORE_PIN8_CONFIG = PORT_PCR_DSE | PORT_PCR_SRE | PORT_PCR_MUX(3);
    uint32_t divisor = ((F_BUS * 2) + ((baud) >> 1)) / (baud);
    if(divisor < 32) {divisor = 32;}
	if (divisor < 32) divisor = 32;
	UART2_BDH = (divisor >> 13) & 0x1F;
	UART2_BDL = (divisor >> 5) & 0xFF;
	UART2_C4 = divisor & 0x1F;
	UART2_C1 = 0;
	UART2_PFIFO = 0;
	UART2_C2 = C2_TX_INACTIVE;
	NVIC_SET_PRIORITY(IRQ_UART2_STATUS, IRQ_PRIORITY);
	NVIC_ENABLE_IRQ(IRQ_UART2_STATUS);
}

void LCD_end()
{
	if (!(SIM_SCGC4 & SIM_SCGC4_UART2)) return;
	NVIC_DISABLE_IRQ(IRQ_UART2_STATUS);
	UART2_C2 = 0;
	CORE_PIN7_CONFIG = PORT_PCR_PE | PORT_PCR_PS | PORT_PCR_MUX(1);
	CORE_PIN8_CONFIG = PORT_PCR_PE | PORT_PCR_PS | PORT_PCR_MUX(1);
	UART2_S1;
	UART2_D; // clear leftover error status
}


void LCD_write(const void *buf, unsigned int count)
{
    const uint8_t *p = (const uint8_t *)buf;
	while (count-- > 0) {
	uint32_t head, n;
    if (!(SIM_SCGC4 & SIM_SCGC4_UART2)) return;
	//if (transmit_pin) transmit_assert();-------------------------------not needed because not using receive
	head = tx_buff_head;
	if (++head >= 40) head = 0;
	while (tx_buff_tail == head) {
		int priority = nvic_execution_priority();
		if (priority <= IRQ_PRIORITY) {
			if ((UART2_S1 & UART_S1_TDRE)) {
				uint32_t tail = tx_buff_tail;
				if (++tail >= 40) tail = 0;
				n = tx_buff[tail];
				UART2_D = n;
				tx_buff_tail = tail;
			}
		} }
	tx_buff[head] = *p++;
	transmitting = 1;
	tx_buff_head = head;
	UART2_C2 = C2_TX_ACTIVE;
}
}

void setup()
{
pinMode(8, PUTPUT);
delay(10);
LCD_begin(9600, 0x00);
}

void main() 
{
LCD_write(my_array, 8);
}

but in order to get the LCD_write function to work, I have to

1. rename the tx_buffer[]
2. rename tx_buffer_head & tx_buffer tail,
3. add #include "serial3.c"
4. remove the LCD begin() and LCD_end() functions,
5. and use Serial3.begin(9600). I want stop using this and roll my own.

My question is... what else am I missing that is linked elsewhere in another library in order to accomplish a stand alone function for LCD_write(###, #) with LCD_begin() instead of Serial3.begin().

Please help.
Thanks
BLMinTenn
 
Probably not a good plan to #include the serila3.c file. Normally includes are used only for declaring stuff, not including the actual implementation code.
 
Hey Paul,
I have spent about 3 solid days trying different variants of the begin function() and the LCD does not work. However, If I leave the tx_buffer[] tx_head and tx_tail original and link the serial 3 library, everything works and the LCD comes alive. I can get the LCD_begin(), LCD_end() & LCD_write() functions to compile with no problem.... there's just something beyond my understanding of the code and this darn problem is DOA.

Can you please give me some pointers as to what I have left out in the code in the original post. Keep in mind that I am only transmitting to the LCD one way.

Thanks
BLMinTenn
 
Well Paul, Im one of those kids that takes dads new drill apart just to see how it functions... If I get it back together in one piece, then I definitely know how it works. The drill scenario is as close to the reason why I am trying to make the library stand-alone. Im not trying to re-invent the wheel, I just want to know how the functions link to the core library. Thus far, I have learned how the #defines are linked to the Kinetis registers, ( if I have registers right). This activity has also taught me the importants of classes and how they can make one function call universal among float, integers, longs and bytes.

So, to sum it up, I am trying my best to learn what you have so elegantly put together for this community; IE CODE. Just like curious George with an LCD, Teensy 3.2 and no darn idea of how the LCD, quote "really" interfaces with the low hanging fruit (code).

When you have a few minutes of you busy time. Could you kindly educate an old fart just a little.

Thanks
BLMinTenn
 
For experimenting, you can simply edit serial3.c. Arduino will detect the file as changed and rebuild it automatically. No need to copy & rearrange the code. The Arduino software is designed to make this sort of experimentation easy.
 
Tonton81, thanks, you rock.
A few questions about this code...

Code:
const unsigned long bit = F_CPU / DEBUG_BAUD;

The baud is being clocked by the CPU frequency and not the F_bus? I take it that the code is independent of the that or do I need to change it?

for debugging? Impressive.
Code:
  ARM_DEMCR |= ARM_DEMCR_TRCENA;
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;

so, I take it that this is the format for 8N1.

Code:
uint16_t c = (0x100 | *s++) << 1; //Add start & stop bits

It looks like that this function is similar to bit banging serial. pretty clever code. Now for the stupid question... does it work because I am not near my test bed at the moment.

Thanks
BLMinTenn
 
Credit goes to Frank Boesing for the code (it’s his), but it should work on all Teensy ARM boards
 
well after looking in the arm manual... it looks like that...

Code:
  ARM_DEMCR |= ARM_DEMCR_TRCENA;
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA;

the first part enables and starts a debug monitor and the second part had to do with keeping the baud rate in sync with the DWT cycle counter which further below pauses in the while statement..

Code:
while ( ARM_DWT_CYCCNT - t < (i * bit) ) {;}

WOW, this guy that wrote the code needs employee of the year....

Thanks
BLMinTenn
 
Hey Paul,
Whats your opinion of this code relative to the built-in serial3 library in terms of performance. I dont have a scope to test, but I suppose I could write a program to measure in millis();

Thanks
BLMinTenn
 
That code is bitbanged - using the F_CPU clock [ the ARM clock Cycle Counter ] to control bit toggle to send the data at desired baud rate. Using that code is blocking. Using the Serial# code in TeensyDuino will utilize the hardware engine to drive the UART and feed the hardware by interrupt from the allocated buffer. In the case of Serial3 that hardware is a single byte FIFO - so one interrupt per character - but that gives some 8-10 bit times of non-blocked operation and then the interrupt just moves a byte, updates queue indexes and returns.
 
Defragster,

Thanks for the added knowledge. So, Serial3 library definitely uses a TX and RX buffer, but in theory if I want to break out the code, it looks like I need to also grab the uart2_status_isr() function. Do I have this right?

I am seeing that there's an interrupt per your comment and I assume that the status_isr() functions the on and off of the interrupt.

1. where is uart2_status_isr() called because I cannot see it anywhere
2. Is it somehow controlled by serial_event3() under the yield() function
3. if I wanted to rename uart2_status_isr() just for giggles, how would I re-link it if I have #2 correct?

Not quite there understand it, but hopefully getting close.

Thanks
BLMinTenn (Bryan)
 
@BLMinTenn -
I'd assume anything and everything in Serial3 is essential for rubust/complete usage. Yes it sets up buffer space for Rx and Tx and unique interrupts to handle feeding/pulling the one byte FIFO for the UART hardware behind "Serial3". On T_3's the Serial1 and Serial2 have 8 byte FIFO's and are better devices for general use affording multiple byte transfers between interrupts.

Re: above #2:: Arduino made this thing called serialEvent() that runs at the exit of each loop() pass, and during each yield() [delay()] call to allow that function when USER implemented to service the Serial ports of all types - that is what you see in yield() - so that is optional and required only when using serialEvent() code.

The one odd thing in the Teensy3 source AFAIK is that used or not - perhaps to support the above serialEvent() processing - ANY and ALL Serial? code - and buffers allocated - is included/loaded during compile/link/startup - so unless that code is forcibly removed/replaced including the interrupt vector table init that 'native' PJRC Serial3 code will be ready to OWN that UART device. ANY attempt to rename or repurpose Serial code will have to do that in a way agreeable to that build process. That relates to how any new names for _isr() code or other.


That should be enough to start your journey - it is what I have picked up in my time here with causal observation. Some of it may be less than complete - but like all the other device code from PJRC it just works optimally to interface the Teensy Hardware to the C/C++ world of Arduino with net effect of bare metal efficiency. The code is open source and a local copy is installed for your use/abuse. If you find a bug or a way to improve it PJRC considers errors or PULL requests.
 
Defragster,

Understand now. So, I guess that there's no chance in &*LL to re-link the_ISR() due to what I call being coded in the burried arduino code. I guess beyond this point will require a special rewrite of the new _ISR() name I decide.

Well, I understand now. Since the "drill is together and working" I know when to stop and just accept the code as-is.

Thanks
BLMinTenn
 
I've not seen where Arduino imposes any overhead on Teensy as PJRC does it - except the yield() to support serialEvent() - which is 'weak' and can be removed with "void yield();" in sketch.

Arduino just gives a common easy to use and complete interface to all the MCU hardware … faster hardware with better supported features at that. And complete source is provided to read or tweak. Much better than the MCU maker's raw tools - as Paul has done all the power up init - written a good USB stack and provided robust ability for brick free programming over USB - at a cost lower than a wimpy 8 bit 16 MHz UNO. And then there is the FORUM support which you can tell I enjoy greatly :)

With the Teensy '4' now in Beta Paul jumped on a 600 Mhz processor over a year ago while under NDA and not even public - and has been working through making it compatible/adaptable to everything before it - had over a year of dev into it before shipping out Beta boards for group tweaking to an already stable core code to expand the peripheral interfaces. In that year they finally evolved the RAM doubled to 1 MB pin compatible bigger brother with some other adds and it looks like Paul will be getting his initial shipment of those MCU's and updating all his plans to use that better device in the end with more dev work and time on his part before he can ship it later this year.
 
Im loving this code.....

Another question.... do I have this right. I assume that the original code only prints characters and only one character.
Code:
//smallest software serial : (c) Frank B
#define DEBUG_BAUD (115200)

void _serialDebugPrint(const uint8_t pin, const char * s) {
  const unsigned long bit = F_CPU / DEBUG_BAUD;
  ARM_DEMCR |= ARM_DEMCR_TRCENA; // <- may be not neeed
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; // <- may be not neeed
  pinMode(pin, OUTPUT); // <- needed only once
  digitalWriteFast(pin, 1);// <- needed only once
  
  while (*s) {
   uint16_t c = (0x100 | *s++) << 1; //Add start & stop bits
    noInterrupts(); //<-- not needed in NMI
    unsigned long t = ARM_DWT_CYCCNT;    
    for (int i = 1; i<11; i++) {
      digitalWriteFast(pin, (c & 1) );
      c = c >> 1;      
      while ( ARM_DWT_CYCCNT - t < (i * bit) ) {;}
    }
    interrupts();//<-- not needed in NMI
  }
}

If I want to make a write a function that prints out multiple bytes, it would look like the following...
Code:
uint32_t bit;

void LCD_begin(uint16_t baud) {
  bit = F_CPU / baud;
  ARM_DEMCR |= ARM_DEMCR_TRCENA; // <- may be not neeed
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; // <- may be not neeed
  pinMode(8, OUTPUT); 
  digitalWriteFast(pin, 1);// <- needed only once
}

void LD_write_fast(const void * buf, uint8_t count) {
  uint16_t c;
  uint8_t i;
  uint32_t DWT_time;
  while(count--) {
    while(*buf++) {c = (0x100 | *buf++) << 1; //Add start & stop bits
                           __asm__ volatile("CPSID i":::"memory");    //__disable_irq()
                           DWT_time = ARM_DWT_CYCCNT;    
                           for(i = 1; i<11; i++) {digitalWriteFast(pin, (c & 1) );
                                                  c = c >> 1;      
                                                  while( ARM_DWT_CYCCNT - DWT_time < (i * bit) ) {}
                                                  }
                           __asm__ volatile("CPSIE i":::"memory");    //__enable_irq()
                          }
    }
}

right? or not.
 
Last edited:
Understand now. So, I guess that there's no chance in &*LL to re-link the_ISR() due to what I call being coded in the burried arduino code. I guess beyond this point will require a special rewrite of the new _ISR() name I decide.

We do have an attachInterruptVector() function, which lets you commandeer an interrupt vector, even if another library has already defined a function with the reserved name. See kinetis.h for details.

On Teensy 4 we're moving away from reserved names, to using only attachInterruptVector(). The reserved names are a sort of legacy, from the earliest days of Teensy 3.0 before allocating the interrupt vector table in RAM (and when we had only 16K RAM), from before the dynamic assignment of DMA channels, and trying to do things following the traditions of older 8 bit chips.

Of course use attachInterruptVector() with caution on Teensy 3. If the other code has already initialized the hardware and enabled the interrupt, attachInterruptVector() give you plenty of rope to hang yourself.

Every Teensy has a pushbutton dedicated to getting you back into programming mode, so you can recover from mistakes that prevent automatic reboot from just clicking the button in Arduino.


Well, I understand now. Since the "drill is together and working" I know when to stop and just accept the code as-is.

If you love reinventing wheels or you just want to tinker with direct hardware access, you certainly can. Just make sure to set your expectations realistically for the amount of help we can give you here. The existing code is very mature, so most of the "help" we can offer will look like suggestions to use the tried-and-true code.
 
Im loving this code.....

Another question.... do I have this right. I assume that the original code only prints characters and only one character.
Code:
//smallest software serial : (c) Frank B
#define DEBUG_BAUD (115200)

void _serialDebugPrint(const uint8_t pin, const char * s) {
  const unsigned long bit = F_CPU / DEBUG_BAUD;
  ARM_DEMCR |= ARM_DEMCR_TRCENA; // <- may be not neeed
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; // <- may be not neeed
  pinMode(pin, OUTPUT); // <- needed only once
  digitalWriteFast(pin, 1);// <- needed only once
  
 [COLOR=#ff0000] while (*s) {[/COLOR]
   uint16_t c = (0x100 | *s++) << 1; //Add start & stop bits
    noInterrupts(); //<-- not needed in NMI
    unsigned long t = ARM_DWT_CYCCNT;    
    for (int i = 1; i<11; i++) {
      digitalWriteFast(pin, (c & 1) );
      c = c >> 1;      
      while ( ARM_DWT_CYCCNT - t < (i * bit) ) {;}
    }
    interrupts();//<-- not needed in NMI
[COLOR=#ff0000]  }[/COLOR]
}
No, take a look at the while(*s) - it prints a whole string until a zero is reached. For binary data, you should replace this.
Interrupts are enabled between the characters. - yes it's blocking - in most cases, this is OK, esp. if you use it to print strings for debugging.
It is possible to write a non-blocking version by calling a timed interrupt for every bit.
You can use the printf format, if you create the string with sprintf()
 
Last edited:
Frank B,
Your code is really nice and small. After hitting the reply button, I have since corrected the code with a new version. Since you are on the line... did you ever implement a version for for receive?

Thanks
BLMinTenn
 
No, the time I wrote that I did not need receive.
But this would be quite similar - you'd need an interrupt ( with attachInterrupt() ) that triggers with the startbit (if you don't want to wait for characters) and then just do the same as above with digitalRead and silghtly modified logic (ingnore start/stop-bits, digitalRead in the middle of a bit)

Edit: a nice exercise ;)
 
Last side note - you might look at SoftwareSerial library - I never have - but it implements serial in software - but on Teensy it only does that on pins with hardware serial support - I suppose because the hardware uses timers and is dedicated as needed to Rx and Tx bit in parallel - as noted this special purpose Tx only code is blocking - including interrupts during the char Tx bits. That must be resolved in the SoftwareSerial library.
 
Yes, but i see no reason to use software if you have so many hardware serials on the teensy.. (well, with the exception of a few special cases, such as debugging)
(or if you need 23 serial TX ;-)
 
Status
Not open for further replies.
Back
Top