Novinka:STM32 křížem krážem X
(Kategorie: STM32)
Napsal gripennn
05.07.2019 00:00

Malá poznámka o načítání dat do MCU rovnou z multimetru a pokus ověřit jak je na tom vestavěný DAC s přesností.

Multimetr jako ADC pro STM32 (+ přesnost DAC)

Motivace

V některém z mých předchozích příspěvků se rozvinula debata na téma přesnosti DAC na STM32. Nějakou dobu vrtalo mi hlavou jak pokud možno bezpracně ověřit přesnost v celém rozsahu. Nejsem majitelem žádné přesné ani nepřesné měřicí karty, takže mě pořád dokola napadalo jen připojit multimetr k PC a nějak z něj tahat data. To je docela pracné řešení když uvážíte, že musíte napsat dva programy, jeden pro STM a jeden pro PC. Což je jistě pro někoho hračka, ale já na PC programuji jen velmi zřídka, takže jsem našel následující alternativní variantu. Multimetry posílají data do PC skrze optočleny tvořené infradiodou v mutlimetru a fototranzistorem/fotodiodou v přijímacím modulu ("USB káblíku"). Není tedy vůbec složité přijímat data pomocí STM a zbavit se tak potřeby psát nějaký program pro PC. Což oceníte zvlášť v situaci kdy si s mikrokontroléry rozumíte víc jak s počítačem.

Příprava

Minimálně dva multimetry (Axiomet a UNI-T) co jsem v labu našel posílají skrze infraport data "protokolem" UART. Navíc v ASCII podobě takže jejich rozklíčování je triviální záležitostí. Nemůžu zaručit, že nenarazíte na multimetr který bude data posílat v nějakém nečitelném binárním protokolu, ale očekávám, že UART-ASCII kombinace bude docela standard. K testu jsem si vybral UT71A, neboť se mi zdálo, že jeho přesnost (disp 19999, 0.1%+8dig) bude stačit. Než se pustíme do samotného měření musíme překonat tři vcelku jednoduché problémy. Prvním z nich je jak z optického signálu udělat konvenční UART čitelný STMkem. Díky tomu že je "infraport" velice slušně krytý před okolním osvětlením lze k tomu využít celou řadu metod s pomocí fotodiody nebo fototranzistoru. Namátkou například tyto:

  • Fotodiodu můžete zapojit jako "solární článek", tedy anodou ke GND a napětí na katodě porovnávat s nastavenou úrovní pomocí komparátoru. Výstup z komparátoru bude signál pro UART. Díky tomu, že komparátor máte přímo na STMku, nebudete krom fotodiody potřebovat žádné další součástky. Tento postup má dvě nevýhody. K vnitřnímu nastavení komparační hladiny spotřebujete jeden DAC kanál (signál z fotodiody může mít tak nízké napětí že nelze využít ani hladinu 1/4 Vrefint). Na některých STM (jmenovitě STM32F303K) nemá komparátor volitelnou hysterezi (nejspíš nemá žádnou když ji datasheet neuvádí), takže na jeho výstupu budou vznikat zákmity, které ale díky svému krátkému trvání UARTu nevadí. Oba problémy lze snadno odstranit doplněním externích součástek, ale ztrácí se ta elegance.

  • Můžete využít vnitřní operační zesilovač a zapojit ho jako "transimpedance amplifier". Jinak řečeno připojit ho jako na schématu níže. Zpětnovazební odpor zvolit tak velký aby OZ pracoval až do saturace a výstup opět přivést na Rx UARTu. Krom fotodiody vám k tomu postačí jediný rezistor. Nevýhodou, z principu věci, je fakt že k tomu spotřebujete OZ, který umí spolupracovat s ADC a může se tedy stát objektem kalibrace.

  • Můžete udělat totéž jako v předchozím bodě ale s pomocí externího OZ. Nevýhodou je že budete muset osadit víc součástek, výhodou je že pak takové řešení můžete připlácnout k libovolnému MCU. Já zvolil právě tuto variantu a zapojil "převodník světlo-napětí" dle následujícího schematu. Jako OZ posloužil MCP6002 jakožto laciný zástupce Rail-to-Rail operáků.

Schema zapojení

Druhým problémem je navázat fotodiodu na infradiodu v multimetru. Což se naštěstí ukázalo jako velice jednoduché a postačil k tomu kus kartonu. Nejlépe to popisuje následující fotografie.

Kousek kartonu vymezuje polohu fotodiody proti IR diodě v multimetru
Celá sestava před měřením bez bufferu. Na pomocné DPS je zesilovač ze schematu.

Třetím a posledním problémem je zjistit komunikační protokol. V datasheetu k mutlimetru jsem ho nenašel (a ani jsem nečekal že tam bude, ale možná jsem jen špatně hledal). Na první pohled se signál jevil jako UART s 2400b/s v klasické kombinaci 8n1 (8bit, žádná parita, 1 stop bit). Což se jevilo jako správný tip dokud multimetr neposlal jiný znak než "0". Po velmi nepříjemné půlhodince jsem pochopil, že jde ve skutečnosti o 7o1 (7bit, lichá parita, 1 stop bit). Zpráva se stává z 11ti bytů. První pětice je obsah displeje multimetru, pak zvolený rozsah (multimetr je typu auto-range), pak nějaké detaily a celá zprava je zakončena kombinací \r\n viz odkaz.

Oscilogram zprávy z multimetru. Vpravo si můžete všimnou konfigurace 7bitů, lichá parita, 1 stopbit (2400 Baud)

Program

Program pro STM32F303K8 je naprosto přímočarý. Postupně inkrementuje výstup DAC od nuly do maxima, čeká na výsledek z multimetru a posílá ho UARTem na terminál v PC. Protože multimetr odesílá svá data až po skončení měření (a mezi tím nejspíš měří další) tak program první výsledek po změně DAC vždy vyřadí. Multimetr posílá výsledky měření přibližně 3x-4x za sekundu a MCU nemá žádnou kontrolu nad tím kdy přijdou. Proto probíhá příjem pomocí přerušení. Signál z operačního zesilovače (na obrázku výše) je nutné invertovat pomocí funkce USART_InvPinCmd(). Program přečte prvních 5 znaků zprávy (obsah displeje) a převede je na celé číslo. Podle šestého znaku zjistí nastavený rozsah multimetru, z tabulky restable[] vyčte jednotku a složí výsledek měření do typu float. Nakonec napětí odešle spolu s hodnotou DAC na terminál v PC. Použití float by se dalo na tomto místě vyhnout, ale pokud by měl přístroj provádět nějakou "samokalibraci", určitě se bude dělat snáze ve floatech.

// Ověřování přesnosti DAC pomocí multimetru s "optovýstupem"
// Multimetr UT71A měří výstupní napětí DAC a vysílá infradiodou "do světa"
// opt. signál z mutlimetru vede do fotodiody a zpracován triviálně pomocí OZ (current to voltage converter)
// a je následně přijímán UARTem 1, výsledky kalibrace jsou odesílány UARTem 2 do PC
// AVDD = 3.318V, výstup DAC s bufferem veden na střed děliče 27k(GND)-27k(VDD), nucleo32 kit (F303K8)

// UT71 A protocol (7o1 2400 baud/s), message 11 byte in format:
// Dig1,Dig2,Dig3,Dig4,Dig5,Range,Function, State1, State2, \r, \n
// PA10 Usart1 Rx - Opamp output (from multimeter)
// PA2 Usart2 Tx - to PC
// PA4 DAC1_OUT1

#include "stm32f30x.h"
#include "stdio.h"
#include "string.h"
#include "stdlib.h"

void dac_init(void);
void usart1_init(void);
void usart2_init(void);
void USART_puts(volatile char *s);
void calibrate(void);
void dac_init(void);

// pro příjem UARTu 1
#define MAX_STRLEN 32
volatile char message[MAX_STRLEN]; // zpráva od multimetru
volatile uint8_t new_message=0; // příznak příchodu zprávy (flag)
const float restable[5]={0.1e-3,1e-3,10e-3,0.1,0.1}; // tabulka rozlišení multimetru [V]

int main(void){
 SystemCoreClockUpdate(); // 8MHz
 usart1_init(); // USART1 (příjem dat z multimetru pomocí IRQ)
 usart2_init(); // USART2 (odesílání výsledku měření do PC)
 dac_init(); // Výstup DAC který testujeme
 calibrate(); // jednorázové měření celého rozsahu DAC

  while (1){
    // konec programu
  }
return 0;
}

void calibrate(void){
uint16_t dac_raw = 0; // hodnota v DAC
char txt[20]; // pracovní řetězec
volatile uint32_t value; // přijatá hodnota z multimetru v integer formátu
float meas, base; // přijatá hodnota napětí z multimetru + jednotka napětí z multimetru
uint8_t res; // přijaté informace o aktivovaném rozsahu multimetru

// procházej celý rozsah DAC
while(dac_raw<4096){
 DAC_SetChannel1Data(DAC1,DAC_Align_12b_R,dac_raw);  // nastav hodnotu na výstup DAC   
 while(!new_message); // počkej na první zprávu z multimetru...
 new_message=0; // ... a ignoruj ji
 while(!new_message); // počkej na druhou (už relevantní) zprávu z multimetru...
 strncpy(txt,message,5); // vyčti prvních 5 znaků (číselné vyjádření změřené hodnoty)
 txt[5]=0; // korektní zakončení řetězce
 value = atoi(txt); // převeď řetězec na číslo
 res = message[5]-48; // zjisti zvolený rozsah
 base = restable[res-1]; // zjisti si jednotku z tabulky rozlišení
 meas = base*(float)value; // spočti výsledek měření jako desetinné číslo
 snprintf(txt,sizeof(txt),"%u %1.4f\n",dac_raw, meas); // pošli zprávu o výsledku měření...
 USART_puts(txt); // ... UARTem do PC (v praxi lze nahradit například zápisem do flash,eeprom a pod.)
 new_message=0; // zprávu od multimetru jsme zpracovali ...
 dac_raw++; // přejdeme na další hodnotu DAC
 }
}

// inicializace DAC a jeho výstupu
void dac_init(void){
GPIO_InitTypeDef gp;
DAC_InitTypeDef dac;

// zapnout GPIOA a DAC1
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC1, ENABLE);

// PA4 (DAC1_OUT1) - analog, výstup DAC
gp.GPIO_Pin = GPIO_Pin_4;
gp.GPIO_Mode = GPIO_Mode_AN;
gp.GPIO_OType = GPIO_OType_PP;
gp.GPIO_PuPd = GPIO_PuPd_NOPULL;
gp.GPIO_Speed = GPIO_Speed_Level_1;
GPIO_Init(GPIOA, &gp);

// konfigurace DAC
dac.DAC_Buffer_Switch = DAC_BufferSwitch_Disable; // nevypínat buffer
dac.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0;  // nezájem
dac.DAC_Trigger = DAC_Trigger_None; // žádný trigger
dac.DAC_WaveGeneration = DAC_WaveGeneration_None; // wave generation nepoužívat
DAC_Init(DAC1,DAC_Channel_1,&dac); // aplikovat nastavení na kanál1 DAC1
DAC_Cmd(DAC1,DAC_Channel_1,ENABLE); // spustit DAC1
}

// inicializace USART2 (odesílání dat do PC)
void usart2_init(void){
USART_InitTypeDef usart_is;
GPIO_InitTypeDef gp;
// zapnout GPIOA a USART2
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 , ENABLE);

// PA2 RX USART2
gp.GPIO_Pin = GPIO_Pin_2;
gp.GPIO_Mode = GPIO_Mode_AF;
gp.GPIO_OType = GPIO_OType_PP;
gp.GPIO_PuPd = GPIO_PuPd_NOPULL;
gp.GPIO_Speed = GPIO_Speed_Level_1;
GPIO_Init(GPIOA, &gp);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_7);

// konfigurace (38400 Baud, 8n1)
usart_is.USART_BaudRate = 38400;
usart_is.USART_WordLength = USART_WordLength_8b;
usart_is.USART_StopBits = USART_StopBits_1;
usart_is.USART_Parity = USART_Parity_No ;
usart_is.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart_is.USART_Mode = USART_Mode_Tx;
USART_Init(USART2, &usart_is);
USART_Cmd(USART2, ENABLE);
}

// Odesílání řetězce do PC na terminál
void USART_puts(volatile char *s){
 while(*s){
  while (!USART_GetFlagStatus(USART2,USART_FLAG_TXE)){}
  USART_SendData(USART2, *s);
  s++;
 }
}

// inicializace USART1 (příjem zpráv z multimetru)
void usart1_init(void){
GPIO_InitTypeDef gp;
USART_InitTypeDef usart_is;
NVIC_InitTypeDef nvic;

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// PA10 (Rx))
gp.GPIO_Pin = GPIO_Pin_10;
gp.GPIO_Mode = GPIO_Mode_AF;
gp.GPIO_OType = GPIO_OType_PP;
gp.GPIO_PuPd = GPIO_PuPd_NOPULL;
gp.GPIO_Speed = GPIO_Speed_Level_1;
GPIO_Init(GPIOA, &gp);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_7);

// Baudrate 2400, 8n1 (v reálu 7o1), invertovat Rx pin
usart_is.USART_BaudRate = 2400;
usart_is.USART_WordLength = USART_WordLength_8b;
usart_is.USART_StopBits = USART_StopBits_1;
usart_is.USART_Parity = USART_Parity_No ;
usart_is.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart_is.USART_Mode = USART_Mode_Rx;
USART_Init(USART1, &usart_is);
USART_InvPinCmd(USART1,USART_InvPin_Rx, ENABLE);
// deaktivuji overrun (zjednodušuje debug, neboť tok dat z multimetru nejde regulovat)
USART_OverrunDetectionConfig(USART1,USART_OVRDetection_Disable);
// přerušení od příjmu
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
nvic.NVIC_IRQChannel = USART1_IRQn;
nvic.NVIC_IRQChannelPreemptionPriority = 1;
nvic.NVIC_IRQChannelSubPriority = 1;
nvic.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic);
// Spustit USART
USART_Cmd(USART1, ENABLE);
}

// rutina přijmu znaků z multimetru
// Řetězec se přijímá do rx_string a po skončení se kopíruje do "message"
void USART1_EXTI25_IRQHandler(void){
 static char rx_string[MAX_STRLEN];
 static uint16_t cnt = 0;
 char t;
 if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE)){
  t=USART_ReceiveData(USART1); // vyčíst přijatý znak
  t = t & 0x7f; // odstranění paritního bitu
  // testovat konec zprávy
  if((t!='\r') && (t!='\n') && (cnt < (MAX_STRLEN-1))){
   rx_string[cnt] = t; // uložit přijatý znak
   cnt++;
  }
  else if(cnt>0){ // jakmile je zpráva celá...
   strncpy (message,rx_string, cnt); // zkopírovat ji...
   message[cnt]= '\0'; // zakončit ji tak aby z ní byl plnohodnotný řetězec...
   cnt = 0;
   new_message++; // ... a informovat program že přišla
  }
 }
}




Výsledky

Multimetr je relativně pomalé měřidlo, takže projít celý rozsah trvalo přibližně tři čtvrtě hodiny. Pro zajímavost jsem proměřil výstup DAC se zapnutým i vypnutým bufferem. Výstup s bufferem jsem zatížil děličem 27k/27k (VCC/GND). Pro snadnější orientaci jsem čárkovanou čarou vyznačil úroveň referenčního napětí (strop DAC). V grafu odchylky si můžete všimnou že od určité hodnoty jsou výsledky z multimetru "zašuměné". To je dáno změnou rozsahu z 2V na 20V.

Výsledky měření s bufferem pracujícím do děliče 27k/27k (VCC/GND).
Zelená stopa vyjadřuje "ideální" závislost výstupního napětí na kódu DAC
Červená stopa je změřená závislost výstupního napětí na kódu DAC
Černá stopa je odchylka od ideální hodnoty (mV), růžově vyznačeno 1LSB
Výsledky měření s vypnutým bufferem bez zátěže.
Zelená stopa vyjadřuje "ideální" závislost výstupního napětí na kódu DAC
Červená stopa je změřená závislost výstupního napětí na kódu DAC
Černá stopa je odchylka od ideální hodnoty (mV), růžově vyznačeno 1LSB

Odkazy

Další tutoriály lze nalézt na www.elektromys.cz
| V1.00 4.7.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /




Tato novinka je z -MCU-mikroelektronika
( http://mcu.cz/news.php?extend.4094 )