Highty Ho Boys and girls, more embedded C fun in the land of audioDiWhy.
I'll be back with more hardware builds next month, but for now I continue to experiment with embedded C code for the Raspberry Pi RP2040 processor.
This week I ported an AD9833 library I wrote for Atmel MPU's to RP2040.
It's ready to test--however the new library has 25 methods and counting. How do I test the library without having to rewrite and recompile main.c over and over?
I used UART. This is the protocol that flowed between your 80's era PC clone and a <15K baud dial up modem. Useful things stick around; UART is simple, well documented, and supported in some manner on many computers and development boards.
An informative webpage regarding how UART works is here.
GET IT WIRED UP
RP2040/pico SDK makers can configure serial via USB configured through CMakeLists.txt but I found serial UART from my RP2040 developpment board simpler and more accurate once everything was set up correctly.
I am using a Raspberry Pi for sending and receiving UART serial communications from development boards. Setting a Raspberry Pi 4 up for serial wasn't as simple as I thought it'd be, but the video here helped.
To wire it up:
I created a 3 conductor wire with expando sleeving and dupont connectors for TX > RX, RX > TX, and GND < > GND. Note that you must connect ground between the host and the development board for any of this to work.
Assuming you are using my code: there are different UART pins on the Pico; you have to use the right ones. I used GPIO "0" or Pin 1 for RX; GPIO "1" or Pin 2 for TX, on the PICO. This conforms to defaults set in pico.h in the SDK.
On the RPi, I used GPIO pins 14 for TX and 15 for RX. This is UART0.
On the RPi4, install minicom if it's not already there. Settings are 115200 baud rate, and 8n1
To this end, make sure your CMakeLists.txt has "I want UART for serial, not USB" set correctly:
#control USB output--1 means on.
pico_enable_stdio_usb(${PROJECT_NAME} 0)
pico_enable_stdio_uart(${PROJECT_NAME} 1)
Wiring up the Pico for UART.... |
I use SSH to get to this Raspberry Pi SBC and then use Minicom as the terminal program |
USAGE
UART and so on in the Pico API are documented here. This is a subsection of the PICO hardware API docs, here.
You can get the UART.c and UART.h files used in this post, ready to drop into your project, here. The basic use case for the library is documented in the library's UART.h.
Here is basic syntax:
/*
put this in endless loop in main
READ_UART_BUFFER;
// trap a matched string you type into your terminal program:
printf("%s\n",uart_buffer); // show what you just typed
if (UART_READ("REBOOT"))// reboot right from minicom!!!!
{
reset_usb_boot(0,0); reboot PICO
}
else
{
// any other entry into the buffer does whatever is coded in //here.
}
}
*/
AD-WHY? C WHAT? DEVWHAT?
Why do any of this at all? (the audiodiWhy mantra....)
The AD9833 is a function generator chip--it has a reasonably wide frequency range (.1hz to 12.5Mhz), multiple waveforms, reasonably low noise, 2 registers for phase, sleep modes, and so on. I hoped to build a volt/octave FM synthesis board and other waveform generators using AD9833's along with RP2040 development boards; for that I figure I need a working AD9833 library.
There are already Arduino libraries for the AD9833 IC (here), and I assume other popular microcontrollers, but maybe not for the RP2040. (Really? Not sure. I didn't en endlessly google RP2040 C AD9833 or whatever....so maybe a working PICO <> AD9833 C library was already out there? In any event, it wouldn't hurt to roll my own as a learning experience....)
A few mornings later the library was written and I was ready to test...so I started modifying main.c to test each method.
So:
tri();
after recompiling changed the waveform at output of the AD9833 to a triangle. Works!
Next in main.c I added
squarehalf();
and recompiled; this altered a square wave to .5* of the frequency found in either frequency register. This worked as well.
phase1(1300);
along with recompiling put a 14 bit phase value into the IC's phase 1 register. That didn't work quite right so i tweaked it, recompiled, and after that yes, it worked.
You get the idea.
AD9833 Breakout Board |
With all of these methods to test, modifying main.c, then re-compiling, then re-uploading to the Pico over and over became tedious.
How could this be streamlined?
For inspiration I didn't go out in nature--I didn't breathe the fumes--I didn't even talk to fellow geeks.
Instead I turned to youtube. Specifically the video here.
The content creator uses a UART serial stream to turn off and on LEDs on a Pico dev board, but as he mentions: there are other use cases. Testing libraries could be one of them.
So: I got this going on my bench.
After configuring a Raspberry Pi 4 I had lying around, I wired the UART TX and RX to the RX and TX on the Pico following the steps at the beginning of this post.
Finally I SSH'd from my Ubuntu bench virtual machine to the Raspberry Pi, then once shelled in, ran minicom. Minicom of course can send data over serial by typing characters into its terminal, which I did to kick off each function under test.
CODE ME UP ANNIE!
In a header file I called uart.h, at the top of the file, I declared a 1K global array, then wrote a global macro to compare a scanf statement to the contents of the newly created buffer. This almost straight out of the aforementioned "low level learning" video, so again, if you've made it this far in today's post, you should consider watching it, the video is here.
// global buffer for uart reads
char uart_buffer[1024];
//define macro for endless loop in main
#define READ_UART_BUFFER scanf("%1024s",uart_buffer)
//test to see if string X is in buffer.
//Do whatever is in curlies {...} if there is a match
#define UART_READ(X) strcmp(uart_buffer, X)== 0
in main.c's main loop, I trapped a match between a string and what is in the uart_buffer.
while (1 < 2) {
READ_UART_BUFFER;
if (UART_READ("TRI")) //gen tri wave
{
AD9833_Tri();
CNTL_SEND; //macro to send via SPI the control uint16_t
}
AD9833_Tri() was supposed to change whatever waveform the AD9833 was generating to a triangle. Hooking the IC's output did the sine change to a triangle? Not at first, so I stopped and worked on the method--and no need to change main.c--and quickly had the function up running.
Next I expanded this paradigm so I could extract parameters from the serial data. UART only sends strings between systems, so, how could I trap numbers (decimals) to use as function parameters?
For example, I wanted to test the AD9833's FREQ0 register with 0x4000 0x4000. Right after, I wanted to test 0x4000 0x5000.
I could have written switch statements, or a lot of if ()'s in main.c.
Instead I created a new file, uart.c and wrote functions based on strncmp--on a match, the functions extracted the ASCii style numbers; turned them to decimals and sent the uint32_t's to a function under test.
Here's the code:
uint8_t compare_strings(char *a, char *b, uint8_t len)
{
uint8_t x = 0;
if ((strncmp(a,b,len))==0)
{
x = 1;
return x;
}
else
{
x = 0;
return x;
}
}
//////////////////
uint32_t dumpleft_conv2dec(char *hi, uint8_t chopleft, uint8_t digits)
{
uint8_t i, len = 0;
uint32_t result = 0;
char *x = hi;
uint8_t count = 0;
count = chopleft;
do
{
{
*(x + count - chopleft) = *(x + count);
}
count++;
}
while (*(x+count) != '\0');
//deal with end of string
if (*(x + count) == '\0')
{
*(x + count - chopleft) = '\0';
}
//turn ASCII numbers into a decimel number
for (i=0; i< digits; i++)
{
result = result * 10 + ( x[i] - '0' );
}
return result;
}
compare_strings matches string array *a to *b from left to right, up to len characters. If there is a match, it returns a 1. dumpleft_conv2dec then strips whatever characters I didn't want from the left side of the string (chopleft), and turned the rest of the string into a decimal value of length digits (keeping in mind that char "1234" did not directly translate to dec 1234--ASCii uses different codes for decimal characters!)
Now I can type FRQ02000 into minicom and main.c finds a match, gets rid of the "FRQ", and turns 02000 into decimal 2000.
From here it's trivial to use this as a parameter to test a function:
char f[4] = "FRQ"; // this has to be +1 greater than # of chars when creating array
if (compare_strings(uart_buffer,f,3)) // compare first 3 chars
{
uint32_t rez = 0;
rez = dumpleft_conv2dec(uart_buffer, 3, 5);
AD9833_LoadFreq0(rez);
FREQSEND; //macro to send via SPI 4x freq bytes to register
}
Ha! This worked!
THE BIG LIMITATION
Putting a scanf() into the loop in your main.c means your loop will stop at the scanf(), waiting for input. This means your main.c loop won't loop.
This worked for testing this library, but for other applications, say, reading a rotary encoder, probably not--you need your loop to loop! For that I can see using an interrupt and not scanf() when UART data comes in.
I will probably experiment with modifying the code to use interrupts in an upcoming post.
ARDUINO AGAINO?
Another use for the UART paradigm is to run a high power processor on the back end and use something TX ready, like a Pro Micro, on the front end/user interface. The Ardunio handles the switches, pots, LEDs and OLED displays.
As long as the end user can tolerate a brief delay--for example, a waveform that takes 100-200ms to change--an Arduino could send the same ASCII strings to the backend processor used during testing--we already know it works!
However, I have a faint recollection that RX and TX commuincation step on Arduino bootloaders--so you can't program an Uno R3 with the RX and TX pins connected to something sinking current. Why do I think that? I don't remember, and maybe I'm wrong. I hope I am. I guess we'll see.