2

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

L9780 datasheet

/* 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;
}
asked Jul 20, 2024 at 1:13
3
  • 1
    1) 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)); Commented 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. Commented 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! Commented Jul 20, 2024 at 14:33

1 Answer 1

0

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

answered Jul 24, 2024 at 12:49
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.