Globally enabling/disabling CH32V003 interrupts with the mstatus CSR - Page 1
Forum section: electronics / general

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 22, 2023, 08:05:03 pm
content: I'm attempting to put together some universal use-case code for disabling and enabling interrupts globally on the CH32V003, for the purposes of having an 'atomic' section of code that cannot be interrupted. Something like avr-libc'sATOMIC_BLOCKconstruct. The primary aim is to make it universal so that it can be used within ISRs without messing things up.Now, it seems a common way to globally disable interrupts on the CH32V003 is to toggle the MIE flag of the mstatus register. Indeed, there are functions__disable_irq()and__enable_irq()in the WCH HAL/SPL that do just this. However, they are kind of a brute-force way of doing it because they simply blindly clear and set MIE (and MPIE for some reason) regardless of its prior value.To begin with, I want to confirm that my understanding of the flags in the mstatus register is correct. Firstly, because the CH32V003 operates exclusively in Machine mode, I only need be concerned with the M* fields, correct? Second, is regarding the relationship between MIE and MPIE. Am I correct in understanding that MPIE serves to save the value of MIE when an interrupt occurs, and that MIE gets restored to the value of MPIE by the MRET instruction when returning from an ISR? However one thing confuses me. The CPU manual says (section 7.2, p29): "it should be noted that in the QingKe V2 series microprocessors, MIE is not updated to 0 at this time before entering the last level of interrupt to ensure that the interrupt nesting in Machine mode continues to be executed". What does this mean? Are they trying to say that normally (assuming nesting is enabled), MIE isnotset to zero during an ISR, but in one circumstance it is? What is this "last level" of interrupt? Are they saying that when an interrupt of the lowest priority occurs, MIEdoesget set to zero? Is that the absolute lowest (i.e. 15) or whatever the configured lowest is?Anyway, regardless of that, what I think I need to do to implement a kind of atomic block macro is to save and restore the MIE, as follows:1. Read the value of mstatus and save the MIE value.2. Clear the MIE bit.3. Execute the block's code.4. Set MIE back to whatever the saved value is.This way, I don't believe it will matter what context this atomic block construct is used in - outside of or inside of an ISR. I don't believe I need to do anything with MPIE, nor with MPP (apparently it only can be and accept a value of 0b11).What I have come up with to implement this is:Code:#define interrupt_atomic_block() \for( \uint32_t __interrupt_atomic_block_mie_saved = __interrupt_save_clear_mie(), __interrupt_atomic_block_loop = 1; \__interrupt_atomic_block_loop; \__interrupt_restore_mie(__interrupt_atomic_block_mie_saved), __interrupt_atomic_block_loop = 0 \)#define MSTATUS_MIE_MASK 0x8__attribute__((always_inline)) static inline uint32_t __interrupt_save_clear_mie(void) {uint32_t value;__asm volatile("csrrc %0, mstatus, %1": "=r" (value) // Outputs: "i" (MSTATUS_MIE_MASK) // Inputs: // No clobbers);return value & MSTATUS_MIE_MASK;}__attribute__((always_inline)) static inline void __interrupt_restore_mie(uint32_t value) {value &= MSTATUS_MIE_MASK;__asm volatile("csrs mstatus, %0": // No outputs: "r" (value) // Inputs: // No clobbers);}Is all my understanding correct? Are there any flaws in my plan or implementation? Any advice gratefully received.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 22, 2023, 08:37:25 pm
content: By the way, anybody know why objdump's disassembler does not seem to recognise RISC-V CSR instructions?For example, for me it only outputs the aforementioned CSRRC instruction as ".4byte 0x300477f3".Do I need to tell it what RISC-V ISA and/or extensions I'm using? Looking at the --help output, it says there is a -Mpriv-spec=SPEC option, but I have no idea which value to pass (it says supported values are "1.9.1 1.10 1.11 1.12") for the WCH QingKeV2, which apparently conforms to the older ISA spec version 2.2.Edit:If I pass in -Mpriv-spec=1.12, I get a warning of "mis-matched privilege spec set by priv-spec=1.12, the elf privilege attribute is 1.11". So if it knows that from the ELF file, why doesn't it decode the CSR instructions? Explicitly passing -Mpriv-spec=1.11 doesn't change anything though.

author: SiliconWizard
author_rank: Super Contributor
timestamp: March 22, 2023, 08:49:49 pm
content: Do you happen to know what version of binutils you are using?

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 22, 2023, 09:14:18 pm
content: Quote from: SiliconWizard on March 22, 2023, 08:49:49 pmThe output of --version is "GNU objdump (xPack GNU RISC-V Embedded GCC x86_64) 2.38".

author: SiliconWizard
author_rank: Super Contributor
timestamp: March 22, 2023, 09:27:55 pm
content: Quote from: HwAoRrDk on March 22, 2023, 08:05:03 pmThat looks correct. It's a relatively common way of doing this, even if it's a bit drastic (sometimes you may want to disable interrupts on a lower level, depends on the application.)But certainly that will prevent the block of code to be interrupted. It would not prevent an exception, though.Quote from: HwAoRrDk on March 22, 2023, 08:05:03 pmThe problem I see at first sight is with your __interrupt_restore_mie() function.(I suppose the 'csrs' is a typo and it's 'csrrs'?)If it's strictly used in the context you describe (ie. the MIE bit would be cleared when calling __interrupt_restore_mie()), then it should work fine.But strictly speaking, if __interrupt_restore_mie() is called, the MIE bit is currently set and the original value (passed to the function) is 0, it won't clear the MIE bit.Probably just a detail - you may not have intended this function to allow that. But taken independently, this function would have a misleading behavior: if 'value' is 0, it has no effect.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 22, 2023, 11:40:29 pm
content: Quote from: SiliconWizard on March 22, 2023, 09:27:55 pmY'know, I'm still not clear about the difference between interrupts and exceptions.As I understand it, an exception will trigger an interrupt, but all interrupts are technically exceptions?If exceptions are caused by stuff like illegal instructions, misaligned memory accesses, etc. then I guess nothing to really worry about there.Quote from: SiliconWizard on March 22, 2023, 09:27:55 pmNo, that's supposed to be exactly what's written. CSRS is a pseudo-instruction to set bits but without reading the value of the register first. It appears to get expanded by the assembler to a CSRRS with a destination register of x0 - i.e. the zero register, which is of course read-only.Quote from: SiliconWizard on March 22, 2023, 09:27:55 pmAh, you're right. Yes, that is a slight flaw. I don't intend to use__interrupt_restore_mie()outside of the atomic macro, so MIEshouldalways be zero when the function is called, so it doesn't really matter that it can only set and not clear. I can't imagine how MIE would become set in-between. I mean, the whole purpose is to disallow any other code from being executed, and if it can then I've failed my objective.I suppose I could modify__interrupt_restore_mie()to first do a CSRC to ensure MIE is clear, and then do CSRS to set it according the argument value, but I'm in two minds as to whether it's worth it.

author: brucehoult
author_rank: Super Contributor
timestamp: March 23, 2023, 12:27:40 am
content: Quote from: HwAoRrDk on March 22, 2023, 11:40:29 pmPrecisely. Plus EBREAK and ECALL.Incidentally, memory access exceptions (misaligned, protection violation, page fault) are the only data-dependent exceptions in RISC-V. All others are detected at instruction decode time. Data-dependent things such as arithmetic overflow, divide by zero, floating point "exceptions" (which actually set flags) produce defined results and carry on. You can't get aborts of things late in the execution pipeline.

author: Sacodepatatas
author_rank: Regular Contributor
timestamp: March 24, 2023, 02:18:43 pm
content: QuoteIf I recall right, there is an "undefined/unimplemented instruction" exception that can be used for software extending the ISA. For example, if the code within the ch32v003 traps such exception whenever a MUL opcode is found (invalid since the V003 hasn't got a multiplier), then this operation can be transparently implemented in the exception handler (with the corresponding runtime penalty, of course).

author: SiliconWizard
author_rank: Super Contributor
timestamp: March 24, 2023, 08:25:53 pm
content: Yes this is the illegal instruction exception (xcause = 2, usually there is only machine mode on MCUs, so just 'mcause').When the exception is raised, mepc will contain the address of the illegal instruction and mtval *may* contain the instruction itself (that part is optional so implementation-dependent.)

author: HwAoRrDk
author_rank: Super Contributor
timestamp: March 25, 2023, 09:09:35 pm
content: As an aside, working on this interrupt stuff has given me an insight into what is being done in the WCH-supplied start-up assembly code as to how interrupts get globally enabled there. I think what I have determined is correct - obviously please correct me if I'm wrong.The only thing the start-up assembly code does related to enabling interrupts is the following:Code:li t0, 0x80csrw mstatus, t0That sets MPIE in the mstatus register. It puzzled me as to how that resulted in interrupts being enabled, because it's only MPIE being set, and not MIE.However, I believe the answer to that lies further on in the code, where main() is called - which is another source of puzzlement, because a conventional calling method is not used.Code:la t0, maincsrw mepc, t0mretHere the code loads the address of main() into the mepc register, then executes an mret. What this is doing is essentially 'returning' to main(), as if the start-up code were a sub-routine. When the mret instruction executes, it takes the value of MPIE and restores it to MIE, thus enabling interrupts.It's clever, but I'm not sure why they chose to do it that way - as opposed to, say, just explicitly setting MIE and calling main() conventionally. Perhaps because this way results in MIE getting set no earlier than the transfer to main()? But I don't know why that would matter, seeing as no interrupts could possibly be configured to occur before any user code has configured them to. Any ideas?

author: SiliconWizard
author_rank: Super Contributor
timestamp: March 25, 2023, 11:43:07 pm
content: I wouldn't call that clever.But certainly would be curious to know what their rationale was.

author: brucehoult
author_rank: Super Contributor
timestamp: March 26, 2023, 01:00:17 am
content: Quote from: SiliconWizard on March 25, 2023, 11:43:07 pmIf nothing other than what is quoted above is mucking withmstatusthen every field exceptMPIEis set to 0. That means in particular thatMPPis00, so when themretis executed the CPU will switch to User mode to executemain.

author: SiliconWizard
author_rank: Super Contributor
timestamp: March 26, 2023, 01:02:51 am
content: Quote from: brucehoult on March 26, 2023, 01:00:17 amDoes the CH32V003 support user mode?

author: brucehoult
author_rank: Super Contributor
timestamp: March 26, 2023, 01:18:22 am
content: Quote from: SiliconWizard on March 26, 2023, 01:02:51 amHmm, no idea. Maybe not. They probably use the same startup code on all their cores.If U mode is not implemented thenMPPwill be hard-wired to 11 and moving 00 to it will have no effect. You can read it back to check.

author: SiliconWizard
author_rank: Super Contributor
timestamp: March 26, 2023, 01:26:41 am
content: Quote from: brucehoult on March 26, 2023, 01:18:22 amYeah, I just checked in the DS and it only supports machine mode. As I suspected.But using the same startup code for all their MCUs looks like a possible rationale here.I don't even know if they have a single product that supports user mode though. But that's the only reasonable explanation.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: April 01, 2023, 08:56:04 am
content: I had the idea to take the technique for an atomic block macro I talked about in my first post in this thread, and to adapt it to have a macro that can make an atomic block that disables/restores only a specific individual interrupt, using the PFIC control registers.What I came up with is this:Code:#define interrupt_individual_atomic_block(irq) \for( \uint32_t \__interrupt_atomic_block_irq_saved = (PFIC->ISR[(irq) / 32] & (1 << ((uint32_t)(irq) % 32))), \__interrupt_atomic_block_loop = (PFIC->IRER[(irq) / 32] = (1 << ((uint32_t)(irq) % 32))); \__interrupt_atomic_block_loop; \(PFIC->IENR[(irq) / 32] = __interrupt_atomic_block_irq_saved), __interrupt_atomic_block_loop = 0 \)What it does is:1. Saves the current enabled status value of the appropriate PFIC_ISRn register, masked to the given interrupt number.2. Disables the given interrupt using the relevant PFIC_IRERn register by writing 1 in the appropriate bit. This non-zero expression value is also assigned to the loop flag so the loop body will proceed.3. Re-enables (if previously enabled) the given interrupt by writing the previously-saved value to the appropriate PFIC_IENRn register.4. Exits the for-loop by setting the loop flag to false.However... there's one thing that this relies upon, which I have some uncertainty about: the order in which the local variable declarations are processed.It's obviously critical here that the interrupt's current enabled status is savedbeforeit's disabled - i.e.__interrupt_atomic_block_irq_savedis assigned its value before__interrupt_atomic_block_loopis assigned its value. Does the C standard guarantee that? I think so, but I'm not 100% sure.I imagine it does because when declaring multiple variables in a single statement you might want to initialise the value of one with the value of a prior one, and that wouldn't work if the compiler didn't strictly order the initialisations left-to-right. But I can't find anything explicitly saying so. Nearest I found was oncppreference.comwhere it says about declarators that "The end of every declarator that is not part of another declarator is a sequence point". IIRC, a 'sequence point' essentially means everything must be resolved before proceeding to the next thing, but reading the page linked for "sequence point" is giving me a headache.Is that sentence confirming the ordering behaviour is as I think it is?

author: brucehoult
author_rank: Super Contributor
timestamp: April 01, 2023, 09:03:05 am
content: Quote from: HwAoRrDk on April 01, 2023, 08:56:04 amCorrect.At least for everything that has or can observe side-effects. For other things the order doesn't matter.