Porting SC20260N: TinyCLR 2.3.0 → 3.0.1

Porting SC20260N: TinyCLR 2.3.0 → 3.0.1

Migrating a fielded handheld from 2.3.0 to 3.0.1.6001 on SITCore SC20260N, one shared source tree
building both. Below is only what did not go smoothly, grouped by where it stands. Happy to post
a minimal runnable repro for any of these - say which and we will put it up.


[Summary by Claude, who did most of the work! :grinning_face_with_smiling_eyes: ]

Still open

1. Internal temperature sensor (ADC3 channel 18) reads ~15 C high

Same board, same room, flipping between a 2.3 and a 3.0 build within minutes - far too fast for a
real change in die temperature.

Identical arithmetic both sides, same calibration registers (ts1 = 12429, ts2 = 16441, read
from 0x1FF1E820 / 0x1FF1E840), and ResolutionInBits reports 16 on both.

build raw reported
2.3, one conversion ~12730 ~35 C (21 C room, 30 C module surface by IR)
3.0, one conversion ~5400 -110 C
3.0, plateau after 5+ conversions ~13450 ~50 C

On 3.0 the channel needs about five back-to-back conversions to stabilise, and any pause resets
it
. Eight successive ReadValue() calls, then one more taken immediately after printing them:

P1: 9654 11798 13048 13480 13370 13461 13428 13452
T = -110.75 C (raw=5370)   <-- read taken right after printing the line above

A single Debug.WriteLine was enough to drop it from 13452 back to 5370. On 2.3 none of this
happens: one conversion on a freshly opened channel is correct immediately.

To us that looks like insufficient acquisition time on a channel the STM32H7 reference manual says
needs a long sampling time.

  • Did ADC sampling time, clock prescaler, or oversampling change for the internal channels in 3.0?
  • Is there any supported way to set sampling time from managed code?

Note the plateau is still ~5.7% high in raw counts, so “settle it for longer” does not look like
the whole answer.

2. Power.Sleep / RTC wake

We sleep in repeated 30 second slices with a watchdog rather than one long Power.Sleep, because
long sleeps did not reliably wake (2.2-era issue). Does 3.0.1 change this on SC20260N? We would
like to drop the chunking - it costs real battery life on a handheld.


Worked around, cause not understood

3. Dispatch appears to run the wrong overload’s body (byte vs string)

Every WiFi request threw CLR_E_WRONG_TYPE at the first access of a byte[] parameter:

void WriteWithTimeout(byte[] Data, int timeout)
{
    var n = Data.Length;        // throws here

Data.GetType().FullName, logged immediately before that line, returns System.String. It is
declared byte[] and every caller in the chain passes a byte[].

The class declares two methods named WriteWithTimeout differing only by a byte[] versus
string first parameter (and a second pair follows the same pattern). Renaming so that no two
methods share a name fixed it completely. We applied the rename to 2.3 as well, in case it is
latent there.

  • Is this known in 3.0?
  • Is the risky pattern narrower than “same-named methods” - base/derived, or across an interface?

We have not isolated a minimal repro yet, but will build one if this is not already familiar.

4. API changes we adapted to

  • Net.WebUtility no longer publicly exported.
  • Cryptography.SHA1 now internal; public SHA1 moved to a separate assembly.
  • WatchdogController.Enable takes TimeSpan, not uint ms. An improvement, just a compile break.
  • Templates default LangVersion to 12 where 2.3 defaulted to 9 - a surprise in a shared-source
    build.

Ours, not yours - pointers for anyone else porting

Both were real bugs in our own 2.3 code that 2.3 tolerated silently for years.

5. DacController.OpenChannel now validates the index

We passed SC20260.GpioPin.PA5 (= 5) where SC20260.Dac.PA5 (= 1) was wanted; the part has two
DAC channels. 2.3 accepted it silently, 3.0 throws - correctly. The trap is that both constants are
named PA5 in different nested classes, so the wrong one reads perfectly plausibly at the call
site.

6. Two threads on one ADC controller, unsynchronised

A battery monitor reading VRef2p5 continuously on its own thread, plus on-demand temperature
reads elsewhere, each with their own AdcController and AdcChannel against the same physical
ADC3, with nothing serialising them. On 3.0 this gave wildly inconsistent readings including large
negatives. One shared lock fixed it, and we applied it to 2.3 too - the hazard is real there, the
window just never seems to have been hit.

Worth auditing before you port if two subsystems in your codebase happen to touch one controller
independently.

1 Like

Nice, thks for sharing, we will look into 1,2,3 and 6.
4, 5 are NAB.

1 Like

Aren’t that sweet, having a well knowledged assistant on your side? We love it.

1 Like

Thanks - and agreed on 4 and 5 being NAB, those were ours. Three things since.

A correction to item 6, since you are looking into it

I said we had applied the shared ADC3 lock to our 2.3 build as well, on the grounds that the race
must be real on both. That was wrong and we have reverted it on 2.3.

On 2.3 the 2.5V reference read and the temperature read had worked correctly, concurrently, for
years. Adding the lock did not help and removing it did no harm - the exceptions we were chasing
turned out to have a different cause entirely, below. The lock stays on 3.0, where it did resolve
the wild swings.

ADC reads fail transiently when the USB mass storage device reconfigures

This is what we were really chasing, and it looks like the more interesting half of item 6. Seen on
2.3.0, so not a 3.0 regression, and it survived every change at our end.

Whenever our MassStorage device reaches Configured, an AdcChannel.ReadValue() on a background
thread can throw:

System.InvalidOperationException - CLR_E_INVALID_OPERATION
GHIElectronics.TinyCLR.Devices.Adc.Provider.AdcControllerApiWrapper::Read [IP: 0000]
GHIElectronics.TinyCLR.Devices.Adc.AdcChannel::ReadValue [IP: 0012]

The correlation is exact - it lands on the Configured transition and at no other time. It affects
both controllers: in one run we caught it on a Controller1 read and, on the next loop
iteration, on a Controller3 read. It is transient, and the following reads succeed.

Our thread only reads two channels every 100ms - battery voltage on ADC1, a 2.5V reference on ADC3

  • with both channels opened once at startup and held. We never touch the ADC from the USB path.

Not urgent for us: it is caught, and a stale sample costs us nothing. But it looks like USB device
reconfiguration transiently invalidating the ADC, which would matter a great deal to anyone
sampling continuously. Is there clock or power-domain reconfiguration around USB device state
changes that could reach the ADC peripherals?

A small request: SCSI vendor/product strings on MassStorage

Windows registers our device with an empty vendor field:

USBSTOR\Disk&Ven_&Prod_Anodemeter_uSD&Rev_1.00\...

SCSI INQUIRY defines that as 8 bytes of vendor identification, so an empty one is not really legal,
and it makes the device harder to identify in Device Manager and in logs.

MassStorage exposes exactly one overload in both versions:

2.3.0.1000:  AttachLogicalUnit(storage)
3.0.1.6000:  AttachLogicalUnit(storage)

The NETMF/G120 API we came from has the richer form, and we still pass proper strings there:

AttachLogicalUnit(_ps, 0, "C-Born", "AnodeMeter Drive");

Could an overload taking vendor / product / revision be added?

For completeness, since it may help others doing USB mass storage on SITCore: the VendorId and
ProductId set in UsbClientSetting do not reach the bus in MassStorage mode - Windows sees
VID_1B9F&PID_F007 rather than the values we supplied. Not asking for that to change, but it means
every SITCore mass storage device shares one VID/PID and the serial number is the only thing
distinguishing one unit from another
. We had been shipping the same serial on every unit, so
Windows treated them all as one device. Worth flagging for anyone else in that position.

All of this is in 3.0.2.1000, just released.

1 — Fixed. The internal temperature sensor, internal reference voltage and VBAT channels were never actually connected to the ADC, and the VREFINT and VBAT channel numbers were swapped in the Pins library. Both corrected .

2 — Could not reproduce. That was a 2.2-era issue and a lot has changed since. We tested on 3.0.2.1000: Power.Sleep woke reliably every time — 5/15/30/60/300 s plus 6 × 30 s, no misses.

There is a related trap that still applies, though. DateTime.Now does not advance across sleep, so Power.Sleep(DateTime.Now.AddSeconds(30)) in a loop falls further behind the RTC on every iteration and eventually asks for a time already in the past — which never matches, and looks exactly like “it doesn’t wake”. Arm from rtc.Now.AddSeconds(...), or call SystemTime.SetTime(rtc.Now) after each wake. You can then drop the 30 s chunking.

3 — Fixed. Overloaded virtual methods could bind to the wrong overload.

4 — NAB. as agreed — an improvement rather than a bug.

5 — NAB. 3.0 behaves correctly here.

6 — Could not reproduce. The ADC fix in item 7 below may help. If you can still reproduce it, send us a simple example and the workaround you’re using.

7 — Fixed. AdcChannel.ReadValue() on a background thread could throw CLR_E_INVALID_OPERATION exactly when MassStorage reached Configured. The conversion had actually completed — the poll was reporting a false timeout, so a good result was rejected. Verified with 528 reads across repeated enumeration and replugs: zero exceptions.

8 — Fixed. AttachLogicalUnit() now takes SCSI vendor, product and revision strings:

ms.AttachLogicalUnit(sd.Hdc, "GHI", "MY-PRODUCT", "1.00");
1 Like

Thanks Dat - really appreciate the detail, and the quick turnaround on all of this.

Confirmed fixed on our end, running 3.0.2.1000:

  • #1 (temperature) - reads correctly on a single conversion now, matching what 2.3 always did. Explains everything we were seeing; no amount of tuning on our side could have fixed a mis-wired channel.
  • #3 (overload dispatch) - confirmed via WiFi, which exercises this exact path on every request: connects and runs cleanly now, no CLR_E_WRONG_TYPE. We’re keeping our rename as a permanent defensive measure regardless, since it costs nothing either way.
  • #8 (SCSI vendor/product) - confirmed, Device Manager shows the vendor string correctly now.

Still to put through its paces on our end: #7 (the MassStorage/ADC transient) - haven’t yet exercised that specific scenario, will report back once we have.

Thanks again for looking into all of this so thoroughly - a real help for our migration decision.

2 Likes