Open morcibacsi opened 7 months ago
Hi @morcibacsi, As you've probably found, you can't write a 'variable' to a register, so you need to have both GPIO_SET_0() and GPIO_SET_1() instructions somewhere in your program, and jump to either one based on the value of the bit you're transmitting. To simplify the logic for the ULP, I find it's best to have everything in the uppermost bits, then you can test BIT15 easily (assuming your protocol is MSB first). It looks something like this:
I_MOVI(R0, 0xA3 << 8), // Suppose R0 has the byte to be transmitted, in the uppermost bits
I_STAGE_RST(),
M_LABEL(LOOP),
M_BGE(TX_1, (1 << 15)), // If BIT15 is 1 then TX_1, else continue to TX_0
// Else transmit 0
I_GPIO_SET(pin, 0),
M_LABEL(LOOP_NEXT),
I_LSHI(R0, R0, 1), // Move next bit into BIT15
I_STAGE_INC(1),
M_BSLE(LOOP, 8),
// Done
I_HALT(),
M_LABEL(TX_1),
I_GPIO_SET(pin, 1),
M_BX(LOOP_NEXT),
You will have a lot to keep track of in your loops (pointer to data, total length, current index, actual pin level, as well as timekeeping) so you may need to spill some registers into memory to make it possible. At 125khz you should have plenty of time.
In fact, I've just realised that R0 will be clobbered when you read the pin level, so the above logic (which uses M_BGE which relies on R0) will not work.
You might want to review your data array structure too. Is there a reason why the length is stored with each data byte?
Here's something I've thrown together, untested and probably full of bugs, to give you an idea of how I might approach this:
#define STUFFED_VAL(byte) (((byte) & 0xF0) << 2) | ((~(byte) & 0x10) << 1) | (((byte) & 0x0F) << 1) | ((~(byte) & 0x01) << 0)
// Prepare data structure
static RTC_SLOW_ATTR ulp_var_t ulp_command[1 + 8];
// The first item (ulp_command[0]) is metadata, which is set last
// Data
// Shift these such that the MSB is in bit15, which simplifies logic for the ULP
ulp_command[1 + 0].val = (STUFFED_VAL(0x0E)) << 6;
ulp_command[1 + 1].val = (STUFFED_VAL(0x5E)) << 6;
ulp_command[1 + 2].val = (STUFFED_VAL(0x4C)) << 6;
ulp_command[1 + 3].val = (STUFFED_VAL(0x20)) << 6;
ulp_command[1 + 4].val = (STUFFED_VAL(0x1E)) << 6;
ulp_command[1 + 5].val = (STUFFED_VAL(0x8A)) << 6;
ulp_command[1 + 6].val = (STUFFED_VAL(0x50)) << 6;
// EOF
ulp_command[1 + 7].val = 0x3FF << 6;
// Now set metadata (flag + length) to begin
ulp_command[0].val = (1 << 15) | 8;
const ulp_insn_t program[] = {
M_LABEL(LBL_WAIT_BEGIN),
// Reset Pointer
I_MOVI(R1, VAN_DATA_ARR_OFFSET),
// Wait here until SoC sets flag to transmit
I_LD(R0,R1,0),
I_BL(-1, 1 << 15),
// Clear the flag, leaving only the length, which we keep in R3
I_ANDI(R3,R0,0x7FFF),
I_ST(R3,R1,0),
// ...
// wait bus idle (omitted here)
// ...
M_LABEL(LBL_NEXT_BYTE),
// Increment pointer, load next byte, reset stage (bit counter)
I_ADDI(R1, R1, 1),
I_LD(R2, R1, 0),
I_STAGE_RST(),
M_LABEL(LBL_NEXT_BIT),
#if 1
// Shift out the MSB (BIT15). This both sets up a branch based on the value of this bit AND prepares the next bit into BIT15.
// I *think* this sets ALU overflow flag but can't recall off the top of my head. If this isn't working, use the alternate logic block below.
I_LSHI(R2, R2, 1),
M_BXF(LBL_1BIT),
#else
I_ANDI(R0, R2, (1 << 15)),
I_LSHI(R2, R2, 1),
M_BGE(LBL_1BIT, (1 << 15)),
#endif
// Else 0 bit
I_GPIO_OUTPUT_EN(_txPin),
M_DELAY_US_1_10(8), // TODO: Reduce this delay due to loop overhead
M_BX(LBL_LOOP_BIT_DONE),
M_LABEL(LBL_1BIT),
I_GPIO_OUTPUT_DIS(_txPin),
// You *may* need to delay here depending on bus capacitance and value of pullup resistors.
// Note there is already ~1uS between these instructions due to the relatively low speed of the ULP and memory.
// A loop would be nice but no registers are free so I'll just unroll the loop a few times.
// TODO: Decide what to do in case of bus contention. Notify SOC? Keep retrying?
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
// TODO: adjust the above, including delay durations, until it conforms with expected timing
M_LABEL(LBL_LOOP_BIT_DONE),
I_STAGE_INC(1),
M_BSLE(LBL_NEXT_BIT, 10),
// Byte complete, check if any more bytes remaining.
I_SUBI(R3, R3, 1),
M_BXZ(LBL_DONE), // Done if R3 (remaining) == 0
// Continue to next byte
M_BX(LBL_NEXT_BYTE),
M_LABEL(LBL_DONE),
// ...
I_HALT(),
Lastly, I gather this is an open-drain bus, so you will need to ensure the TX pin is configured in this mode, or use an OUTPUT_ENABLE and OUTPUT_DISABLE approach (instead of SET(0) and SET(1)) as I've done above. Lastly lastly I don't know the function of 'command_type' and so have ignored it.
Wow, thank you very much, you are awesome!
Currently I can't test with a real bus, so I hooked up the esp32 to a logic analyzer to check the output, and I can say that it is almost working. Somehow an extra bit is put on the line which corrupts the second byte (and therefore everything after that), but the timings seems fine. The STUFFED_VAL works well, maybe the loop or the bit shift is wrong? I am still investigating this. Here is a capture with the logic analyzer. The first byte is good, but the second isn't. Before that I marked the extra bit with the orange arrow which shouldn't be there. In that case the value would be fine. (5E instead of 27)
The dots are the sampling points, the x marks the stuffed bits.
You might want to review your data array structure too. Is there a reason why the length is stored with each data byte?
There wasn't I barely had an idea what I'm doing :)
lastly I don't know the function of 'command_type' and so have ignored it.
There is a scenario when another node transmits the beginning of the message (example 0E 5E 4C) and we want to fill the rest of the packet. But this would require a different logic to read what is contained in the incoming first 3 bytes. Currently I don't need this but later I'd like to extend the code to handle this as well. That was the reason for the command_type.
And here is the adjusted code based on your comments.
const ulp_insn_t ulpProgram[] = {
M_LABEL(LBL_WAIT_BEGIN),
// Reset Pointer
I_MOVI(R1, VAN_DATA_ARR_OFFSET),
// Wait here until SoC sets flag to transmit
I_LD(R0,R1,0),
I_BL(-1, 1 << 15),
// Clear the flag, leaving only the length, which we keep in R3
I_ANDI(R3,R0,0x7FFF),
I_ST(R3,R1,0),
M_LABEL(LABEL_POLL),
//While the GPIO is low, continue polling the pin
I_GPIO_READ(_rxPin),
I_BL(-1,1),
//We have a high state, "move into the middle" of the bit
M_DELAY_US_1_10(4),
//Wait for 16 bits of high state to ensure the bus is idle (16 * 8us)
I_STAGE_RST(),
M_LABEL(LABEL_CHECK_IDLE),
I_STAGE_INC(1),
//Wait for 8us to "move into the next bit"
M_DELAY_US_1_10(8),
I_GPIO_READ(_rxPin),
//If the GPIO is low, we need to wait for the bus to become idle again
M_BL(LABEL_POLL, 1),
M_BSLE(LABEL_CHECK_IDLE, 16),
//We have 16 bits of high state, so we can start writing the data
M_LABEL(LBL_NEXT_BYTE),
// Increment pointer, load next byte, reset stage (bit counter)
I_ADDI(R1, R1, 1),
I_LD(R2, R1, 0),
I_STAGE_RST(),
M_LABEL(LBL_NEXT_BIT),
//#if 1
// this method doesn't seem to work
// Shift out the MSB (BIT15). This both sets up a branch based on the value of this bit AND prepares the next bit into BIT15.
// I *think* this sets ALU overflow flag but can't recall off the top of my head. If this isn't working, use the alternate logic block below.
//I_LSHI(R2, R2, 1),
//M_BXF(LBL_1BIT),
//#else
I_ANDI(R0, R2, (1 << 15)),
I_LSHI(R2, R2, 1),
M_BGE(LBL_1BIT, (1 << 15)),
//#endif
// Else 0 bit
I_GPIO_SET(_txPin, 0),
M_DELAY_US_1_10(4), // TODO: Reduce this delay due to loop overhead
M_BX(LBL_LOOP_BIT_DONE),
M_LABEL(LBL_1BIT),
I_GPIO_SET(_txPin, 1),
// You *may* need to delay here depending on bus capacitance and value of pullup resistors.
// Note there is already ~1uS between these instructions due to the relatively low speed of the ULP and memory.
// A loop would be nice but no registers are free so I'll just unroll the loop a few times.
// TODO: Decide what to do in case of bus contention. Notify SOC? Keep retrying?
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
/*
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
M_DELAY_US_1_10(1),
I_GPIO_READ(_txPin),
M_BGE(LBL_WAIT_BEGIN, 1),
*/
// TODO: adjust the above, including delay durations, until it conforms with expected timing
M_LABEL(LBL_LOOP_BIT_DONE),
I_STAGE_INC(1),
M_BSLE(LBL_NEXT_BIT, 10),
// Byte complete, check if any more bytes remaining.
I_SUBI(R3, R3, 1),
M_BXZ(LBL_DONE), // Done if R3 (remaining) == 0
// Continue to next byte
M_BX(LBL_NEXT_BYTE),
M_LABEL(LBL_DONE),
// ...
I_GPIO_SET(_txPin, 1),
M_BX(LBL_WAIT_BEGIN)
//I_HALT(),
};
Hi @morcibacsi, It looks like the extra bit is due to the overhead at the end of each byte as the ULP checks length, jumps to LBL_NEXT_BYTE, loads the next byte, initialises the loop, etc. If things are shuffled around and the 10th bit is given special treatment, you should be able to dial in the correct timings with something like this:
M_LABEL(LBL_NEXT_BYTE),
// Decrement remaining bytes until done
I_SUBI(R3, R3, 1),
M_BXF(LBL_DONE),
// Increment pointer, load next byte, reset stage (bit counter)
I_ADDI(R1, R1, 1),
I_LD(R2, R1, 0),
I_STAGE_RST(),
M_LABEL(LBL_NEXT_BIT),
I_STAGE_INC(1),
// Check if 1 bit
I_ANDI(R0, R2, (1 << 15)),
I_LSHI(R2, R2, 1),
M_BGE(LBL_1BIT, (1 << 15)),
// Else 0 bit
I_GPIO_SET(_txPin, 0),
// TODO: determine how much DELAY required before and after M_BSGE in order to achieve the correct timings
M_DELAY_US_1_10(1),
// Check if this is the 10th bit
M_BSGE(LBL_NEXT_BYTE, 10),
// Else delay a little more...
M_DELAY_US_1_10(2),
M_BX(LBL_NEXT_BIT),
M_LABEL(LBL_1BIT),
I_GPIO_SET(_txPin, 1),
#define M_CHECK_BUS_HI() I_GPIO_READ(_txPin), M_BL(LBL_WAIT_BEGIN, 1)
// TODO: determine how many M_CHECK_BUS_HI to insert before and after M_BSGE in order to achieve the correct timings
M_CHECK_BUS_HI(),
// Check if this is the 10th bit
M_BSGE(LBL_NEXT_BYTE, 10),
// Not the 10th bit so delay while checking bus
M_CHECK_BUS_HI(),
M_CHECK_BUS_HI(),
M_CHECK_BUS_HI(),
M_BX(LBL_NEXT_BIT),
M_LABEL(LBL_DONE),
// ...
I_GPIO_SET(_txPin, 1),
M_BX(LBL_WAIT_BEGIN)
Also ensure that the TX pin is initialised as INPUT_OUTPUT. The "M_BGE(LBL_WAIT_BEGIN, 1)" previously was incorrect and should have been triggering every time. It should be using M_BL instead (ie. branch if the level is unexpectedly low). I note you're still using I_GPIO_SET(1) to drive the bus high, so I'm confused and a little concerned about the configuration here. Is this just for testing? Is it going to be open-drain? What happens if two transmitters drive the line high and low at the same time?
Ahh man, thank you, it would have taken me a fair amount of time to realize what is the problem. Now with a bit of tweaking the delays it works!
This bus uses a twisted pair of wires (like CAN bus) and uses a differential signal pair. In fact CAN transceivers can be used to interface it, so the pin of the esp32 will be connected to a CAN transceiver's TX and RX pins. Currently I am using only a logic analyzer connected to a gpio of the esp32 to test the output, but in the following days I will try to test on real hardware. After that I will get back with the results and the cleaned code. Also planning to open source the finished driver, I hope you don't mind if I'll mention your name.
Thanks again, you are really awesome!
PS: Just out of curiosity, what do you think would it be possible to write a CAN driver with the ULP ? I know the esp32 has a built in driver for it, the question is strictly theoretical.
Nice work! I've never used CAN but from what I've read it supports frequencies as low as 100kHz (or lower?) so, as long as everything else on the bus is happy going this slow, it should be possible.
Hi @boarchuz I finished the library, and made it available here: https://github.com/morcibacsi/esp32_ulp_van_tx thanks a million times again for your help!
Only one last question. I mentioned that in the future I am going to need another frame type (was command_type in my first post). In this case I need to wait for a specific value (green part below) on the bus (to follow the previous example: 0x0E, 0x5E, 4C) and when I detect those values I need to fill the rest of the data (0x20, 0x1E, 0x8A, 0x50).
So basically the red part:
What my theory is: I wait for the bus is free, so the start of the code is the same as before until this line:
...
M_BSLE(LABEL_CHECK_FREE, 16),
// We have 16 bits of high state, so we can start writing the data
M_LABEL(LBL_NEXT_BYTE),
...
But here I would need a branching logic based on the command_type so either do the same as before, or start comparing the bus state with the bit values in ulp_command[] until the third byte (sticking to the example: 0x0E, 0x5E, 0x4C) if all bits are the same, then the rest of the code is the same as before (writing the values from ulp_command[3] until the length). Hmm.. thinking of it: actually I can stop comparing on the first difference between the bus state and the bits in ulp_command[0-2] as the message isn't the one I need to listen for.
So after the long introduction: how can I send through the command_type parameter? Do you see any problems of why the above wouldn't work?
I was the one who was asking on discord on how to bitbang a 125 kbit/sec bus. As I mentioned one byte is transferred as 10 bits because after the 4th and 8th bits there is a bit stuffed which is the opposite of the previous one. To transmit on the bus we need to wait at least 16 bits of high state (16 * 8us). After that we can start transferring, but if we send a high state and we read back a low state on the rx line we need to stop transferring immediately as someone else also is transmitting with higher priority. Also we don't care if we sent a low, but receive a high, that is fine, we can continue.
You suggested to check the ULP coprocessor. After doing some research I found your awesome library. I checked the examples but some things aren't obvious for the first (few) reads. I settled with the ULPAPADriver as that seems the most complex one which has most of the parts I need. Based on that I prepared a library for my use-case, but I am kind of stuck on how to transmit the bits of a byte. I think I managed to write the part where I wait to start transmitting, but I just can't understand how to loop through my data array and write the bits onto the gpio.
Can you take a look and give me a hint how to proceed further?
Here is the code on the main CPU
And here is my ULP code: