Вторая жизнь старому стрелочному мультиметру
Бывает, что у радиолюбителей есть старый стрелочный мультиметр советских времен, лежит где-то далеко на полке. По прямому назначению его уже использовать не хочется, а выкинуть жалко. Так и лежит. В этой статье мы сделаем его модернизацию, а именно – добавим USB, для возможности анализа данных и построения графиков.
Из курса школьной физики вспоминаем принцип работы такого измерительного прибора – ток течет по обмотке, находящейся в магнитном поле, пытается покрутиться и отклоняет стрелку. Первым делом вскрываем мультиметр и смотрим на подключение стрелки.
Подаем ему на измерение напряжение с потенциометра, а другим мультиметром снимаем напряжение между каждым проводом от стрелки и общим. Данные заносим в табличку и строим график.
Выбираем провод с более резким наклоном прямой. В моем случае получилось что при максимальном отклонении стрелки напряжение равно 96,4 мВ. Для оцифровки микроконтроллером мало, но ничего страшного, это напряжение можно усилить операционным усилителем. Подойдет любой ОУ, я взял LM2904, просто потому что у меня такой был. Смотрим документ на микросхему – два ОУ в одном корпусе, максимальное выходное напряжение Vcc-1.5v. Запитывать будем от 3.3 вольт, значит надо подобрать коэффициенты усиления так, чтобы при зашкаливающей стрелке ОУ выдавал максимально возможное напряжение.
Готово. Первый каскад усиливает напряжение со стрелки мультиметра в 10 раз, второй каскад усиливает выход первого в зависимости от настройки потенциометра. Для тестов я собрал все на старом кусочке текстолита.
Проверяем напряжение на выходе усилителя при разных положениях стрелки. Нужно настроить так, чтобы когда стрелка зашкаливает, напряжение продолжало увеличиваться пока она не упрется в ограничитель хода. Теперь это напряжение надо оцифровать. Я взял китайскую платку с STM, купленную на Али.
На борту у нее микроконтроллер STM32F103C8T6. Есть АЦП и USB. Подходит. Для первоначальной настройки предлагаю воспользоваться STM32CubeMX. Включаем тактирование, настраиваем кварцевые резонаторы.
Включаем и настраиваем АЦП, не забываем про прерывание по готовности.
АЦП будет запускаться по событию таймера, настраиваем таймер.
Таймер тактируется частотой 48 МГц, с предделителем 24 и периодом 2000 получится, что он будет запускать АЦП каждую 1 мс. В принципе так часто нет смысла, но мы будем использовать усреднение значений, поэтому пусть будет. Включаем USB, выбираем Custom HID.
Генерируем проект и переходим к написанию кода. Я использовал System Workbench for STM32. Добавляем в «main.c» запуск таймера и АЦП, и несколько глобальных переменных.
/* USER CODE BEGIN PV */
//uint16_t adc_arr[ADC_MAX_CONVERSATIONS];
uint16_t adc_to_send;
uint32_t adc_sum;
uint8_t adc_counter;
uint8_t send_flag;
volatile uint16_t x;
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
HAL_ADC_Start(&hadc1);
HAL_ADC_Start_IT(&hadc1);
HAL_TIM_Base_Start(&htim3);
/* USER CODE END 2 */
В прерывании прибавляем к переменной значение с АЦП и увеличиваем счетчик. Когда счетчик достигнет 200, усредняем значение и перекладываем в буфер для отправки по USB. Поднимем флаг, что пора отправлять.
/* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){
adc_sum+=HAL_ADC_GetValue(hadc);
adc_counter++;
if(adc_counter==ADC_MAX_CONVERSATIONS){
HAL_GPIO_TogglePin(LD_1_GPIO_Port,LD_1_Pin);
adc_to_send=adc_sum/ADC_MAX_CONVERSATIONS;
adc_counter=0;
adc_sum=0;
send_flag=1;
}
}
/* USER CODE END 4 */
В основном цикле все время проверяем флаг, если поднят опускаем и отправляем буфер. Получится что мы будем отправлять значения каждые 200 мс.
/* USER CODE BEGIN WHILE */
while (1)
{
// HAL_GPIO_TogglePin(LD_1_GPIO_Port,LD_1_Pin);
if(send_flag){
send_flag=0;
..USB_Send_report(adc_to_send);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
Дескриптор USB устройства уже создался стм кубом, его трогать не будем, только проверим интервал опроса, должно быть не больше наших 200 мс.
/* 34 */
0x07, /* bLength: Endpoint Descriptor size */
USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
CUSTOM_HID_EPOUT_ADDR, /*bEndpointAddress: Endpoint Address (OUT)*/
0x03, /* bmAttributes: Interrupt endpoint */
CUSTOM_HID_EPOUT_SIZE, /* wMaxPacketSize: 2 Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (20 ms) */
/* 41 */
Далее составим «HID Report» дескриптор в программке HID Descriptor Tool.
Тут мы говорим, что наше устройство сообщает температуру в комнате (Usage). Report Size=8 и Report Count=4 означают, что 32 бита посылается от устройства к компьютеру (Input) и столько же от компьютера к устройству (Output). Нам из этого всего понадобится только 2 байта, остальное на будущее. Сохраняем как заголовочный файл, и копируем в наш код (куб там оставил место в файле usbd_custom_hid_if.c). Так же надо проверить соответствие размеров репорт дескриптора 35 байт и размер буфера под отправку (тут должно быть 5 байт, потому что мы еще указали Report ID – это еще 1 байт в самом начале). Прошьем и проверим, что устройство правильно определилось в системе.
Раскомментируем функцию отправки в файле «usbd_custom_hid_if.c» и заполняем, указав первым байтом Report ID, дальше наше значение АЦП.
/* USER CODE BEGIN 7 */
/**
* @brief Send the report to the Host
* @Param report: The report to be sent
* @Param len: The report length
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
static uint8_t USBD_CUSTOM_HID_SendReport_FS(uint8_t *report, uint16_t len)
{
return USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, report, len);
}
uint8_t USB_Send_report(uint16_t data){
Rep_buffer[0]=0x01;//report id
Rep_buffer[1]=data>>8;
Rep_buffer[2]=data;
Rep_buffer[3]=0xFF;
Rep_buffer[4]=0xFF;
return USBD_CUSTOM_HID_SendReport_FS(Rep_buffer,5);
}
/* USER CODE END 7 */
Проверяем в какой-нибудь утилитке что пакеты действительно идут.
Осталось написать программку под Windows, которая бы все это обрабатывала. Берем любимую среду программирования и библиотеку для работы с HID устройствами. Я взял старенькую Delphi 7 и библиотеку компонентов JEDI VCL. Из нее нужны «TJvHidDeviceController» и «TJvHidDevice». Добавляем обработчик «OnEnumerate» у девайс контроллера, в него по очереди прилетают все HID устройства при вызове энумерации. Остается отфильтровать наше устройство по VID и PID, затем связать с компонентом «TJvHidDevice».
function TUSBMeter.HidControllerEnumerate(HidDev: TJvHidDevice; const Idx: Integer): Boolean;
begin
if (IntToHex(HidDev.Attributes.VendorID,4)=VID)and
(IntToHex(HidDev.Attributes.ProductID,4)=PID) then
begin
if (HidDev.Caps.OutputReportByteLength=OUT_REPORT_COUNT_AMPERAGE) then
HidController.CheckOutByIndex(HidAmperage,Idx);
usb_ready:=true;
end;
Result := True;
end;
Данные будут приходить в обработчик «OnDeviceData». В нем вычисляем из посылки значение АЦП и выводим куда-нибудь для проверки.
procedure TUSBMeter.HidControllerDeviceData(HidDev: TJvHidDevice; ReportID: Byte; const Data: Pointer; Size: Word);
var
buf:^byte;
begin
if (IntToHex(HidDev.Attributes.VendorID,4)<>VID)or((IntToHex(HidDev.Attributes.ProductID,4)<>PID)) then exit;
buf:=Data; // rep
adc_abs:=buf^;
adc_abs:= adc_abs shl 8;
inc(buf);
adc_abs:=adc_abs+(buf^);
inc(buf);
callback;
end;
Теперь надо сделать пересчет, добавим на форму RadioGroup и настроим как на переключателе мультиметра. Я не стал добавлять шкалу сопротивлений, не нужна.
Заведем так же масштабирующий массив для пересчета и массив с единицами измерений.
const
scale_arr: array[1..18] of real = (600,300,150,60,30,15,6,3,0.75,1500,300,60,15,3,0.6,0.12,0.000012,1);
scale_arr_symb: array[1..18] of string[2] = ('V','V','V','V','V','V','V','V','V','mA','mA','mA','mA','mA','mA','mA','uA','');
Для пересчета еще понадобятся два граничных значения acd_min и adc_max. Подключаем потенциометр к мультиметру, выставляем стрелку на 0 и смотрим, что присылается в программу. Если тоже 0 – хорошо, если нет – не беда, подкорректируем. Потом выставляем стрелку на максимум и так же смотрим. Важно чтобы когда стрелка «зашкаливала» значение продолжало увеличиваться, так будет запас. Если этого нет, надо подкрутить потенциометр ОУ. У меня получилось 0 и 2365. Пересчитываем и выводим уже на основное табло.
procedure TMainForm.HID_Callback;
var
s:string;
buf:string;
rec_s:string;
begin
s:=floattostr(((meter.ADC-adc_min)/(adc_max-adc_min))*scale_arr[RadioGroup1.ItemIndex+1]);
buf:=copy(s,1,5);
buf[pos(',',buf)]:='.';
Label2.Caption:=buf;
end;
Большая часть готова, теперь надо прикрутить запись в файл, страницу настроек с сохранением и красивый GUI. Формат файла я взял CSV, так как из него будет легко строить графики в Экселе.
Все, готово. Осталось собрать все в корпус мультиметра, свободного места там полно. Прорезать отверстие под USB шнурок и убрать обратно на самую дальнюю полку до тех пор, пока не понадобится снять долгий график разряда аккумулятора или график потребления тока каким-нибудь устройством.
Надеюсь, кому-нибудь будет полезно.