I'm working on a program to interface with a development board I spun for an L9780 - wideband oxygen controller IC. At this point, I'm trying to get a consistent "status" read back but continue to get some unexpected results that I'll explain below. This is the first "real" time I'm trying to write the SPI sequence for a chip from scratch, which is why I'd appreciate a sanity check!
The first question/verification I had was the SPI mode/setup. Searching around it looks like per the datasheet my clock is idle-low and the first data assertion (on SI) happens prior to the clock edge going high. This should correspond to a MODE0. Section 6.3 lists it as MSB first. The SPI timing table also lists 8MHz for the transfer rate; all the above should give me my config, correct?
My second question/verification was my use of the "SPI.transfer()" function. I made my way through this thread and understand the syntax behind it, so I think that part is ok.
Section 6.3.1 talks about the "REG" bit which is what's used to latch if the next SO frame contains the input reg, or the current "status" reg. This is why in my "get_status" function I'm sending out the request, and then doing another SPI.transfer (via the function) to read the result. Is this correct?
The reason I think I'm having issues is that for the status reg, the MSB is an ignored bit and then 3 version bits. Intermittently, when using my current code to test, I'm getting a change from a dec(1) value to a dec(2) value. This obviously isn't right, so while it appears I'm able to "communicate" in some form with the chip I'm guessing my formatting or byte order is FUBAR somewhere.
Is it the fact that the arduino is a little endian and I'm relating that to a SPI that transmits MSB first? From the various 64b words I've gotten back, that also doesn't appear to be the case, given the last byte also changes.
Also, per the datasheet there are some fixed (CB) bits and the SPI fault bits that should be giving some insight into the health of the current SPI frame. Both of these are giving me issues as well, which is further pointing me at that I've got something messed up with my SPI communication.
All reasons I'd appreciate some help! Below are some "outputs" from my current code just as a sample for what I'm talking about.
SI register on startup (no setting transfer, nothing)
- 0x4503458300C2CD31
- 0x450345A300E26E32
- 0x450345A300E27732
SI register after "send H/W config to L9780"
- 0x370045A30002F933
- 0x370045A30082FA33
status register on startup (no setting transfer, nothing)
- 0x450345A300024632
- Same result, multiple times
status register after "send H/W config to L9780"
- 0x380045A3FFE1FA33
- 0x380045A300E2FA33
- 0x370045A300C2F933
Status register after heatup and "enable VCCS" and "enable stgINRC"
- 0x1A031AA3001AFA33
- 0x0C030CA3007AFA33
- 0x1A0319A300FAFA33
- 0x150315A3009AFA33
/* Program is taken from the development firmware but simplified
* to menu actions for preliminary debug.
*
* NOTES
* -------------
* -HTR PWM output from uC seems to be inverse. When applying a
* 15% duty, the heater draw is higher than 85% duty
*/
//*****includes
#include <SPI.h>
//*****general defines and data
#define serial_baud 9600 //serial baudrate
#define L9780_SPI_SPD 8000000 //SPI Frequency (per L9780 timing diagram)
#define L9780_SPI_ORDR MSBFIRST //define SPI order as MSB first (per L9780 timing diagram)
#define L9780_SPI_MODE SPI_MODE0 //define SPI mode, clock Idle LOW, first CMD sent w/o SCK (per L9780 timing diagram)
#define L9780_SO_SI true //used for "get status" - put the input (SI) register in SO
#define L9780_SO_STAT false //used for "Get status" - put the status register in SO
//pn_SPI0_MOSI 11 //!pinDef: SPI0 master out slave in
//pn_SPI0_MISO 12 //!pinDef: SPI0 master in slave out
//pn_SPI0_SCK 13 //!pinDef: SPI0 clock
#define pn_L9780_CS 10 //!pinDef: chip select for L9780
#define pn_htr_out 9 //!pinDef: manual heater control output
//*****L9780 specific data
#define L9780_rx_reg 0x0800000000000000 //mask to get back the L9780 input register - bitOR
#define L9780_status_reg 0xF7FFFFFFFFFFFFFF //mask to get back the status register - bitAND
#define L9780_VCCS_en 0x0040000000000000 //mask to enable operator - bitOR
#define L9780_STGINRC_en 0x0200000000000000 //mask to enable short to gnd on INRC - bitOR
#define LSU49_start_CFG 0x00040F18192A8040 //LSU 4.9 start config - non-synchronous mode
/* Config bit definitions:
* b63-60 = 0; not used
* b59 = REG; set appropriately when transmitting or receiving
* b58 = 0; INRC pull-down (try initially disabled)
* b57 = 0; INRC short to ground enable (disable on start)
* b56 = 0; INRC gain
* b55 = 0; tag resistor network switch (ch1)
* b54 = 0; VCCS enable (disable on start)
* b53 = 0; VCCS output ch (ch1)
* b52 = 0; VCCS pull-down (try initially disabled)
* b51 = 0; compensation network (chA)
* b50 = 1; VCCS voltage clamp enable
* b49 = 0; VCCS voltage clamp switch (ch1)
* b48 = 0; VCCS voltage clamp symetry (symetric)
* b47 = 0; fault clear (don't clear any faults)
* b46-43 = 0001; purge current (-14uA) (taken from L9780 test docs)
* b42-40 = 111; FV out gain (12) (taken from L9780 test docs)
* b39-34 = 000110;VCCS capacitance (assumed Rtag ~130 ohm)
* b33-32 = 00; measurement clock period 4MHz
* b31 = 0; heater short to battery threshold (250mV)
* b30-24 = 0011001; RCT1 switch time pulse duration (taken from L9780 test docs)
* b23 = 0; operation mode (free-running)
* b22-16 = 0101010; RCT1 bandgap switch pulse duration (taken from L9780 test docs)
* b15-14 = 10; SPI confirmation bits - always 0b10
* b13 = 0; not used
* b12-11 = 00; heater short to battery fault time (80 Tosc)
* b10-8 = 000; INRC switch time pulse duration (taken from L9780 test docs)
* b7 = 0; protection FET ch2 (disabled)
* b6 = 1; protection FET ch1 (enabled)
* b5-0 = 0; not used
*/
void setup() {
Serial.print(F("Initializing uC.............."));
Serial.begin(serial_baud); //start serial. Needed for more than just debug
pinMode(pn_L9780_CS, OUTPUT); // set the CS pin as an output
digitalWrite(pn_L9780_CS, HIGH); // set the CS pin to HIGH (off)
SPI.begin(); // initialize the SPI -> library begin function sets output pins
pinMode(pn_htr_out, OUTPUT); // heater PWM output
digitalWrite(pn_htr_out, HIGH); // turn htr PWM off
Serial.println(F("Init Complete!"));
//****display menu
Serial.println(F("Menu Options"));
Serial.println(F("--------------------------------------------------------------"));
Serial.println(F("1) Init - Send H/W config command to L9780"));
Serial.println(F("2) Init - Heatup WBO2 sensor"));
Serial.println(F("3) L9780 - Enable VCCS (start operation)"));
Serial.println(F("4) L9780 - Enable stgINRC (start closed-loop L9780 control)"));
Serial.println(F("6) L9780 - Get status register"));
//****end display menu
}
void loop() {
while (Serial.available() == false){} //wait for user input
int menu_choice = Serial.parseInt(SKIP_ALL); //get user input
Serial.println(menu_choice);
char c[150]; //char string for menu outputs
switch (menu_choice){
case 1:
L9780_SPI_xfr(LSU49_start_CFG); //set initial config values over SPI
break;
case 2:
heatup_WB02(); //heat-up sequence
break;
case 3:
L9780_SPI_xfr(LSU49_start_CFG | L9780_VCCS_en); //enable VCCS (start operating)
break;
case 4:
L9780_SPI_xfr(LSU49_start_CFG | L9780_STGINRC_en);//enable INRCSTG once started
break;
case 5:
{uint64_t data = L9780_get_status(L9780_SO_STAT); //get current status output
Serial.print(F("Status Reg (64b): 0x")); SerialPrint_u64(data, true);
}break;
default:
Serial.println(F("Input not recognized, please enter a valid number"));
break;
}
Serial.println(F("--------------------------------------------------------------"));//bookend lines
}
/* FUNC: Serial Print unsigned 64b value
* DESC: Fucntion prints an unsigned 64b wide value
* ARGS: v - value to print
* nl - true/false to print a new line
* RETN: none
*/
void SerialPrint_u64(uint64_t v, bool nl){
char c[] = "00000000"; //temp byte to print
sprintf(c,"%08lX", (v>>32)); Serial.print(c);
sprintf(c,"%08lX", v); Serial.print(c);
if(nl){Serial.println();} //if selected, print newline
return;
}
/* FUNC: WBO2 sensor heatup
* DESC: Fucntion called on sensor initialization
* and controlls the initial heatup cycle, including
* the condensation and ramp phase.
* ARGS: none
* RETN: none
* NOTES: PWM out from L780 is inverse from arduino output
*/
void heatup_WB02(){
Serial.println(F("-----Starting Sensor Heatup----"));
int htr_pwm = 211;
analogWrite(pn_htr_out, htr_pwm); //condensation stage
Serial.println("---Condensation Phase");
Serial.print("HTR PWM %: ");Serial.println(htr_pwm*20/51);
delay(11000); //simplified wait for ~20s total for condensation stage to be done
Serial.println("---Condensation Phase End");
htr_pwm = 76; //start ramp phase, start at 70% duty (approx 8.4V)
while(htr_pwm > 0){ //increasing by 2/255 every 250ms is approx 0.4V/s for ramp phase
Serial.print("HTR PWM %: ");Serial.println(htr_pwm*20/51);
analogWrite(pn_htr_out, htr_pwm); //set PWM
delay(250); htr_pwm -= 2; //increase/ramp
}
Serial.println(F("-----Sensor Heatup Complete----"));
return;
}
/* FUNC: L9780 SPI transfer
* DESC: Fucntion sends a SPI command and receives its result
* ARGS: d - data being sent on a SPI transfer
* RETN: spi_buf - data result
* NOTES: Calls "begin transaction" before every send to ensure
* that if another SPI library is being used that updates
* these values, that they're appropriately set for L9780
*/
uint64_t L9780_SPI_xfr(uint64_t d){
uint64_t spi_buf = d; //data to send
//SPI transfer
SPI.beginTransaction(SPISettings(L9780_SPI_SPD, L9780_SPI_ORDR, L9780_SPI_MODE)); //start SPI transaction
digitalWrite(pn_L9780_CS, LOW); // set the CS pin to LOW (on)
SPI.transfer(&spi_buf,8); //pass address of temp data + size (8 bytes for uint64t)
digitalWrite(pn_L9780_CS, HIGH); // set the CS pin to HIGH (off)
SPI.endTransaction(); //end SPI transaction
return spi_buf; //return rx'd data from L9780
}
/* FUNC: L9780 Function - Get status register
* DESC: Fucntion reads the current status register
* ARGS: act - true/false to select what is in SO
* true - RX'd data (current SI)
* false - status register
* RETN: d - SO register
* NOTES: After sending data, per the datasheet the
* next SPI frame will have the associated data
* in the SO depdending on the asserted REG bit.
*/
uint64_t L9780_get_status(bool act){
if(act = L9780_SO_SI){L9780_SPI_xfr(L9780_rx_reg);}//request input reg back
else{L9780_SPI_xfr(L9780_status_reg);} //request status reg back
uint64_t d = L9780_SPI_xfr(L9780_status_reg); //read result
return d;
}
-
11) The datasheet say maximum 8MHz, it doesn't mean you have to run at 8MHz, in fact since you are using an Uno which has a clock speed of 16MHz, you probably shouldn't run the SPI higher than 4MHz. 2) I don't think your code send the correct command due to Endianness. So instead of defining the L9780_status_reg as 0xF7FFFFFFFFFFFFFF, defined it with an array L9780_status_reg[] = { 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, this will ensure the MS bit of the MS Byte get send out first with SPI.transfer(L9780_status_reg, sizeof(L9780_status_reg));hcheung– hcheung07/20/2024 13:46:23Commented Jul 20, 2024 at 13:46
-
@hcheung thanks, some good feedback. I've run some SPI devices capable up around 10MHz before but I never really dug into their library far to see if they were adjusting the prescalar or not; that's a poor assumption on my part, it's never a bad thing to slow it down first so that'll be a good start to check things.jungle_jim– jungle_jim07/20/2024 14:28:35Commented Jul 20, 2024 at 14:28
-
@hcheung Also, thanks for the heads-up on the endianness. That'll be easy to implement and at least provide another known. I had looked into how pointers/addresses worked with big/little endian on the worry I was mixing something up. I wasn't 100% clear on how the endianess worked with a #define that was expanded at compile time and the SPI.transfer order. That's reason why I used a secondary local variable in the function call with the hope was implementing a local var would alleviate issues. I'll try both and see what happens!jungle_jim– jungle_jim07/20/2024 14:33:24Commented Jul 20, 2024 at 14:33
1 Answer 1
At the suggestion of @hcheung I updated my L9780_SPI_xfr
function to shift in the passed 64b word into a uint16_t array and then I also shift that back out into a 64b result. I'm not sure if that's necessarily the most elegant way to do it, but now at least I'm getting expected answers (with one other change).
One other thing that I had mis-understood originally is that this particular chip doesn't really have an "ignore" command so every transmission I need to send the "control word" and then also update the REG
bit to set what gets shifted into the SO register for the next SPI frame. After making those two changes I can request the SI register after sending a command and get the same result echo'd back.
I'm still having some issues to chase down but the actual SPI coms appear to be functional now. When requesting the status register, the SPIF1
fault bit is set, which correlates to a "SPI command data length error" when requesting the status register. The datasheet states that the expected length is [64 + n*8] bits
which I'm not 100% clear on so I need to do some more digging into that. I'm unsure if the "n*8" part is just a nomenclature thing, or if there's other bits it's expecting beyond the 64b of the control/command register.
EDIT:
As a quick follow-up to this: digging into things a bit I realized that the spreadsheet I had set up for generating my control word was 1bit off (curses!). For the 3 conditions that the "error" in the SPIF[1] bit would be set, none of them are the data errors but since the CB[1,0] bits were shifted, it was causing an issue, thus setting the invalid command / CB error (via SPIF1). I'll update after some further troubleshooting