Quantcast
Channel: stm32 – Andys Workshop
Viewing all 25 articles
Browse latest View live

Nanocounter is an accurate frequency counter using an FPGA, STM32 and a bluetooth android app

$
0
0

Here we have a good example of how a requirement for a simple tool spirals out of control and spawns a project that takes months to complete and ends up dwarfing the project that it was originally expected to facilitate. You see, some time ago I was fiddling around with a project, something to do with data logging, probably, I’ve actually forgotten what I was up to.

Said project would have used an MCU to acquire and timestamp data over an extended period of time and I quickly realised that the oscillators and quartz crystals used to generate the clock tree inside an MCU are not accurate enough to track wall-clock time over extended periods.

At the bottom of the accuracy pile are the built-in oscillators that you get inside MCUs that enable crystal-free operation. For example, the high-speed internal (HSI) clock in an STM32F072 has a factory trimmed accuracy range of ±2.9% over a temperature range of -10 to 85°C.

The HSI operates at 8MHz so that means ±23.2kHz. Over the course of an hour this could mean a drift of more than 10 seconds.

What about the external crystals? Well they’re much better with a typical tolerance of ±30ppm and can be found on most cheap MCU development boards.

An 8MHz crystal running with a tolerance of ±30ppm would drift by a tenth of a second per hour or just two and a half seconds per day, temperature fluctuations not withstanding.

So that got me thinking. As long as I’m operating at a reasonably constant temperature then my data logger could apply a periodic correction to my timekeeping clock so that I’d have very accurate timings over a period of days.

But how do I measure the accuracy of the crystal that’s clocking my MCU? The answer is with a frequency counter. Off to ebay I go. There’s a wide range of counters out there, from the cheapest bare-PCB options to the high-end laboratory grade equipment from the likes of HP (I hold out the hope that one day they’ll drop the comedy names and actually be HP again).


Opposite ends of the product spectrum

Occupying the middle ground there’s the no-name VC3165 unit that looked very tempting and can be had for about £60 from Hong Kong.


The VC3165 generic counter

I almost bought it but instead I somehow managed to persuade myself that it’d be a fun and educational project to design and build a frequency counter myself that could be used for general purpose measurement but would contain features tailored specifically to calibrating MCU clocks.

How frequency counters work

The executive summary is that a frequency counter works by counting the edges of the unknown frequency as well as the edges of an accurate reference over a fixed time period. Simple mathematics then tell you what the unknown frequency is, plus or minus an error term. There are a number of different methods for doing this, let’s take a look at them.

For the remainder of this article we will refer to the frequency being measured as the sample frequency and the accurate timebase as the reference frequency.

Direct frequency measurement

The direct frequency method counts the number of edges of the sample frequency observed during a fixed number of periods of the reference frequency.

The formula for calculating the sample frequency is straightforward.


N is the count, period is in Hz

From the above formula you can see that if the reference frequency is 1Hz then the measurement implementation becomes really simple as the number of sample counts observed is the sample frequency and could be output directly to an attached display.

Because the time period is linked to the reference frequency the error term for this method is ±1 tick of the sample clock, making the error term variable for an instrument built to use this method of counting.

Equal precision measurement

The equal precision (or reciprocal) method links the time period to an integer multiple of the sample clock. There is an excellent and very readable PDF available online that describes this method. Click here to read it.

The instrument receives a trigger to start counting pulses but does not do so until the next rising edge of the sample clock. It then starts counting both the reference and sample clock edges using two counters.

After the desired time period has come to an end the instrument stops counting at the next rising edge of the sample counter. The relationship between the time period, the two counters and their associated frequencies is given by the following ratios:


s and ref are the sample and reference signals

Discarding the period and re-arranging the remaining ratios gives us the following formula for calculating the sample frequency:

The advantage of this method is that the error term is ±1 tick of the reference clock and if the reference clock is high enough then the error term can be reduced to a very small value.

My frequency counter design

After studying the above counting methods I decided on the following goals for my frequency counter, which I’m going to call Nanocounter.

  • Very accurate measurement over a range of 1 to 50MHz. This would cover the range of MCU crystals that I’d want to measure.
  • Onboard accurate, but cost effective reference with the option to feed in an external reference clock source.
  • Advanced options including data logging, charting and calibration of the onboard reference.

That should do for starters, let’s see how I get on. This project will call upon a large number of engineering disciplines including circuit design, PCB layout, SMD reflow, FPGA design, C++ programming and java android programming so I should be in for a fun time.

I’ve made the decision to use the equal precision counting method so let’s see how that looks in a block diagram of the whole system.

The input stage

My design should be able to accept a wide range of input signals including sine and square waves that are DC or AC coupled and range over the common voltages used to drive clock signals.

The job of accepting these signals and transforming them into a standard LVCMOS square wave that I can feed to the FPGA falls to the input stage or analog front end.

I’ll need to instantiate my input stage design twice, once for the sample clock input and again for the external reference clock input.

The PLL

The onboard reference clock will be a 10MHz oscillator and the external reference input, if connected, is expected to be of the same frequency. The PLL is used to multiply the 10MHz input up to the much higher reference frequency that is provided as an input to the FPGA. The actual reference frequency will be limited by the counting speed of the FPGA as well as the limits of the selected PLL.

The PLL used for this design will have to accept multiple inputs and have very good jitter characteristics if I am to maintain my goal of very high accuracy.

The FPGA

Here’s the business end of the system. An FPGA is capable of counting extremely quickly, in parallel, and on a cycle-accurate basis. While even the most basic MCUs have edge-triggered interrupts they cannot react quickly enough to count at the speed that an FPGA can achieve.

The MCU

The MCU marshalls all the other components in the system. It communicates with the user and the display, programs the FPGA on startup and holds system calibration data.

The circuit design

So, considering all that I’ve learned so far I came up with a circuit design for the complete system. This isn’t going to be a bargain-basement design. My requirement for high accuracy means that top quality components from the likes of Linear Technology and Analog Devices are going to be making an appearance on this board.




Click for PDF

The USB port and power entry

Power is supplied through a USB mini B socket. The potentially noisy 5V line is smoothed using capacitors and a ferrite bead. I know from my previous experience with FPGAs that they can be rather power hungry. If the power consumption goes above 500mA then I’ll need to restrict usage to USB 3 ports or charging plugs.

I’ve opted to hook up the D+ and D- data lines even though I may not use them in the final design. The USBLC6 ESD protection device from ST Micro provides protection against spikes on the line that may occur during the insertion and removal of the plug.

The sample and reference input stages

The input stage is based around the LTC6957 from Linear Technology. This device is a very low noise buffer and signal distribution device that can accept any sine or logic level less than 2Vp-p and output a fixed logical level.

It’s available in 4 separate versions that can output LVPECL, LVDS or CMOS logic levels. I’ve selected the LTC6957IMS-3 variant that has CMOS output levels and costs about £5 plus tax from Digikey.

Linear Technology produce a very useful design note, DN514, that presents an example input circuit for the LTC6957 that does exactly what I need.

The spark gap, SG2 and ESD protection diode, D2, provide a level of defense against input voltage spikes that could occur during human interaction with the BNC connector. The capacitor C7 AC couples the input signal before it passes through the transformer, T2 and the schottky diodes provide voltage limiting to the inputs of the LTC6957. That’s a very brief summary of the operation. I’d encourage you to read DN514 for the full details as presented by Linear Tech’s engineers.

The LTC6957 comes with 3 selectable narrowband filter options of 500MHz, 160MHz and 50MHz. The best choice will depend on the input signal so I’ve opted to connect the filter pins to MCU GPIO pins so I can offer the choice of filter through the user interface.


LP5907

I’m powering the LTC6957 from an LP5907 ultra-low noise LDO regulator from Texas Instruments.

The input stage in the illustration is the sample input. The external reference input is basically identical. The only difference is that I’ve connected the LTC6957 output shutdown pin to an MCU GPIO pin so that I can shutdown the output when the onboard reference is being used.

The onboard reference oscillator

The stability and tolerance of the reference clock is key to the accuracy of the frequency measurements. Clearly I need something more accurate than the crystals that I seek to measure which means in practice that I need to use either a temperature compensated oscillator (TCXO) or an ovenised oscillator (OCXO).

The price of these devices is in direct proportion to their stability and they get quite expensive really quickly. Since I’m providing the ability to supply an external reference when exceptional accuracy is required I’ve decided to go for a reasonably high end TCXO, the Connor Winfield M100F, which has a stability of ±100ppb and costs about £14 plus tax from Digikey.

More stable TCXOs exist, and then there are the very stable OCXOs but they’re beyond the scope of my requirements for this article. Cheap used OCXOs and VCOCXOs abound on ebay but I’d be very wary of them since the reason they’ve been decommissioned is that their age means that they could have drifted out of spec and for the VCOCXOs that means they could have drifted out of the range of voltage correction.


Connor Winfield M100F

The TCXO works by applying compensation to the output frequency based on a curve of temperature vs. offset that’s preprogrammed at the factory. A TCXO will still drift with change in temperature but it will do so at a much lower rate than a standard crystal (we’ll have some fun demonstrating this in the videos that accompany this article). Better immunity to ambient temperature changes can be had with an OCXO but they’re really expensive from the usual distributors —.

The datasheet for the M100 specifies an output load capacitance of 15pF. Deviation from this value could alter the output frequency by up to 20ppb per pF. I consulted the datasheet for my FPGA and found an input capacitance range of 3-10pF for input pins with no typical value specified. Hoping that it’ll be somewhere around 5pF I added C25 to the output to provide the additional load. It doesn’t matter too much if I’m off by a bit because the error is a constant and can be compensated for in calibration.

The PLL

The purpose of the PLL is to multiply the reference frequency up to 200MHz for the FPGA to use to generate the high speed counter. Anyone familiar with the Xilinx FPGAs might be wondering at this point why I’m not using the built-in Digital Clock Manager (DCM) on the Spartan 3. The blocker for that idea is that the DCM minimum clock input is 18MHz and in any case the output RMS jitter is rather high at around 250ps compared to approximately 1ps for the device used here.

I’ve selected the Analog Devices AD9553 as the external PLL. It’s a very high quality, low jitter device available in an annoying QFN package.

The AD9553 is full of features and must be externally programmed over SPI before it’ll lock on and generate my target frequency frequency.

The Ax and Yx pins can be used by the AD9553 to hard-select from a range of predefined configurations and can be used as an alternative to SPI programming. Grounding all these pins selects SPI as the active configuration mode and gains access to far more options.

The AD9553 supports two separate inputs and has a pin to select between those. I use these to connect the onboard TCXO and the external reference input and use GPIO from the MCU to select between them.

The 200MHz LVCMOS output is presented on pin 27. I placed a footprint on the board for a series termination resistor in case I need to suppress any ringing on the output. The best way to check if you’re going to have signal integrity issues on a high frequency line is to simulate it as a transmission line using the LTSPICE circuit simulator.


I ran a simulation and the projected output signal looked fine to me so R8 is specified to be 0Ω.

C29, C30 and R11 form the external part of the PLL loop filter. There’s a guide to selecting suitable values for these components in the AD9553 datasheet but far better to use the free AD9553 evaluation software from Analog Devices. It’s one of those quirky programs that has limitations and sometimes crashes but contains such critical functionality that you’ll forgive all for the information it provides.

The challenge with the loop filter designer is to get it to generate standard values for the capacitors and resistor. In the end, after spending ages in this tool, I selected the above values. I’d already bought some 1% 1800Ω resistors so it wasn’t hard to find one that measured almost exactly nominal. The caps were harder. I’d selected the loop filter values so that they fell within the 20% tolerance of some X5R ceramics that I already had in stock and so I spent some time measuring caps until I cherry-picked two that fell very close to what I needed. I probably went through 20 each of my 0.22µF and 4.7µF caps before finding some that were within 1% of the loop filter values.

When I’d finished with the loop filter designer the tool was able to generate a very useful interactive block diagram.

The interactive diagram is preloaded with the settings from the loop filter designer so you just have to make any tweaks to the input and output configurations and then it will generate the SPI register settings for you.

I can’t overstate how useful this tool is and Analog Devices deserves a huge thank you for providing it and saving us hours of error-prone poring over datasheets and application notes. I know who’ll be getting my custom in the future.

The FPGA

I selected a Xilinx Spartan 3 XC3S50-5 FPGA to do the grunt work of the high speed design.

This is an older model than the current Spartan 6 generation but I know it well having used it in my FPGA Graphics Accelerator project and besides I have more than 400 of them to find a use for. No that’s not a typo, I actually picked up 450 of these as new and sealed surplus stock on ebay for a bargain some time ago. Look, here they are!


A sea of programmable logic

I/O capacity is no problem in this design. I only need the two clock inputs and some control lines from the MCU. FPGA_CS, FPGA_SCLK, FPGA_MOSI and FPGA_MISO are an SPI interface for talking to the MCU.

The FPGA_CEN is an count enable input pin that will cause counting to start on a rising edge. FPGA_CDONE is an output pin that will transition to high when the gate time is completed.

The model is that the MCU will use the SPI interface to load a gate counter value and then set FPGA_CEN high. When the gate counter is reached FPGA_CDONE will go from low to high and then the MCU will use the SPI interface to read out the two counter values. The FPGA is then idle until the next rising edge of FPGA_CEN.

I follow Xilinx’s guidelines for decoupling which results in rather a lot of capacitors that need to be placed around the device.

This is probably overkill for this design but caps are cheap and the last thing I want to be doing with my prototype is debugging an unstable design due to insufficient decoupling.

Running the rest of the board at 3.3V means that I need three power supplies for the FPGA.

The 1.2V supply is used for the FPGAs internal logic, the 2.5V supply is an auxiliary source of power used primarily to optimize the performance of various FPGA functions such as I/O switching and the 3.3V supply is used to power the output drivers.

This model of FPGA does not persist your design while the power’s off. When you apply power you need to do a bit of a dance with a selection of programming pins to load the design bitstream into the core before it will run. The INIT_B, DIN, CCLK, PROG_B and DONE pins on the left of the schematic image are used for programming and are controlled by the MCU.

The programming mode, called Slave Serial by Xilinx is co-ordinated by the MCU. I store the FPGA design in the MCU flash and load it into the FPGA from there as the first task after reset. There’s a bit of a hurdle to be overcome in that some of the FPGA programming pins are referenced to the 2.5V supply and are not 3.3V tolerant. Thankfully Xilinx recognised this limitation and publish XAPP453, a guide to programming the Spartan 3 using a 3.3V controller.

Debugging an FPGA design in-circuit is difficult in the extreme which is why I simulate every aspect of it before going anywhere near a circuit design tool. I’ve included a DEBUG pin attached to an LED that I can use as an indicator to help with any last-ditch debugging.

The MCU

The MCU that I’ve selected for this design is the STM32F072CBT6 in a 48-pin LQFP package. The 48-pin package gives me just enough I/O to complete this design and this 072 model comes with 128kb flash and 16kb SRAM. The large flash space was the key reason for the selection of this model because I’ll be compiling in the FPGA bitstream which will require around 60kb.

A nice extra is the inclusion of a USB PHY in this device. I’ll connect up the USB data lines just in case I decided to do something with them in future. In common with all F0 models, this one runs a core clock of 48MHz and can source that clock from an internal PLL fed by a built-in internal 8MHz oscillator.

Most of the I/O is self explanatory. You’ll see the FPGA, PLL and LTC6957 control pins as well as the USB data lines and the SWD programming pins. I’m including a full 20 pin header that will connect directly to the ST-Link/v2 programming device.

A number of status LEDs are provided. The power LED is self-explanatory. The link LED will provide an indication of recent bluetooth activity. The lock LED will light when the PLL is locked. The xref LED will light when the external reference is selected as the active input and the active LED will light when the FPGA is actively counting.

Bluetooth connectivity is provided by the cheap and easy to use HC-06 module that I’ve used before in my Android Reflow Oven project.


The HC-06 bluetooth board

The HC-06 pairs to an Android device without MCU intervention and, once a connection is established you can just treat it as a 9600/8/N/1 UART.

Bill of materials

Here’s a complete bill of materials for this design. The capacitors should be X5R or X7R. Where tolerance is important I’ve noted it in the description.

IdentifiersValueQuantityFootprintDescription
C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, C12, C19, C20, C22, C23, C24, C28, C33, C34, C35, C37, C40, C44, C47, C48, C49, C50, C51, C52, C53, C54100n320603Ceramic capacitor
C13, C14, C15, C16, C17, C18, C41, C4580603Ceramic capacitor
C21, C36, C39, C4310n40603Ceramic capacitor
C2510p 1%10603Ceramic capacitor
C26, C270.47µ20603Ceramic capacitor
C290.237µ10603Ceramic capacitor
C303.83µ10603Ceramic capacitor
C31, C32, C384.7µ30603Ceramic capacitor
C42, C46, C58, C59, C6010µ50805Ceramic capacitor
C55, C5710µ21206Tantalum capacitor
C5622µ11206Tantalum capacitor
D1, D22SOD-923ESD supression diodes (optional)
D3, D4HSMS-281C2SOT323-3ANdual Schottky diodes
D5, D6, D8, D9, D10LED53mm through hole3mm LEDs, various colours
D711206Surface mount LED
FB1, FB2, FB3, FB4BLM18PG221SN1D40603Ferrite bead
P1, P22BNC PCB ConnectorBNC connector
P312.54mm4x1 HC-06 header
P412.54mm10x2 SWD header
SG1, SG2Spark gap2CustomPCB spark gap
R1, R2, R5, R610040805SMD resistor
R3, R460420805SMD resistor
R743010805SMD resistor
R8, R12020603SMD resistor
R9, R20, R21, R2224040805SMD resistor
R1010k10805SMD resistor
R111.8k10805SMD resistor
R13100k10603SMD resistor
R144.7k10603SMD resistor
R1539010805SMD resistor
R16, R176820805SMD resistor
R1812010603SMD resistor
R1933010603SMD resistor
RESET1CustomPushbutton
T1, T2WBC16-1T2CustomCoilcraft transformer
U1, U2LTC69572MSOP12Dual
Output Buffer/Driver/
Logic Converter
U3, U4, U5LP59073SOT23-5AM3.3V low noise regulator
U6AD95531QFN32 5x5PLL
U10AMS11171SOT223-4N3.3V regulator
X1Connor Winfield M1001Custom10MHz TCXO
P5USB connector1CustomUSB Mini B surface mount
U7STM32F072CBT61LQFP-48STM32 F072 MCU
U8USBLC61SOT23-6USB ESD protection
U9TS1117BCP1TO-2521.2V regulator
U11TS1117CW1SOT223-4N2.5V regulator
U12XC3S501VQ100Xilinx XC3S50 Spartan 3 FPGA
HC06Bluetooth board14x1 2.54mmHC-06 Bluetooth board

Board layout

Now I’ve got a schematic that I’m happy with it’s time to consider the board layout. I’ve already decided that this is going to be a four layer board with a stackup that looks like this.

The main driver for choosing four layers instead of the cheaper two layers is that I’ve got both analog and high speed digital circuits on this board and I really want a continuous unbroken ground plane beneath the top signal layer to help with signal integrity and allow me to have controlled impedance traces. I’ll also benefit from simpler routing around the FPGA with the ability to run all those different power supplies through an inner plane.




Click for a larger image

I want to use the great value prototyping services from PCBWay, ITead, Seeed and others which means that I really need to keep the board size to 10x10cm because there’s a large price jump for four layer boards above that level.

Most of the signals, including all the 50Ω controlled impedance traces are on the top layer with the bottom layer used for additional routing. Large copper fills with multiple vias to the solid ground plane are located near to each of the sensitive components so that they have a low noise, low impedance path to ground.


Local fill with multiple vias to ground

The digital components, primarily the FPGA and the MCU are located such that their return signals through the ground plane do not pass under the analog components. Furthermore, chassis ground for the BNC connectors is isolated from the PCB ground.

My previous FPGA design, the graphics accelerator, drew more current from the 3.3V supply than I was expecting, resulting in the AMS1117 regulator getting rather hot. I’ve learned my lesson from that and have included large exposed copper fills around the FPGA regulators that can be used to dissipate the heat throughout the PCB.


Large heatsink area for the AMS1117 is also mirrored on the bottom layer

The fill next to the AMS1117 is sized to accept one of those little RAM IC heatsinks that you can get on ebay. I don’t know if I’ll need one at this stage but there’s room for overkill on this board so better to be safe than sorry.


Layer stack telltale

Over on the left of the board you can see a row of numbers. This is a trick I use to make sure that the manufacturer gets the layer stackup correct. I include the numbers 1..4 sequentially in the copper on each layer from top to bottom and remove the solder mask from top and bottom and the copper from around the numbers so that I can see through the board when it’s held up to the light. If the manufacturer has got the inner layers the right way around then I’ll be able to view the 1..4 sequence with decreasing brightness from front to back.

I made a small mistake in my implementation of this feature in that I forgot to remove the copper from the inner layers surrounding the numbers. The feature still works though. If the manufacturing is correct then I will be able to see 1,2 when viewed from the top and 3,4 when viewed from the bottom.

The 3D view of a board is not just for show, it’s really useful for checking to see if you’ve made common mistakes such as overlapping component labels or labels straying over vias or pads.

Manufacturing the board

Elecrow is my normal go-to provider for two layer manufacturing because their prices are so good. Four layer is more pricey though and anything above 5x5cm sees a large rise in the manufacturing cost. This board is a 10x10cm layout and at the time of writing PCBWay was the best price available.

I’d never used PCBWay before and a quick google around revealed that the online reviews cannot be trusted because PCBWay will pay reviewers for positive reviews posted on internet forums and websites. I really wish they’d stop this because if the service is good then the reviews will come naturally. I’m going to relay my honest experience here with the hope that you can trust me not to make stuff up just to get a few dollars off my next order.

Anyway, I picked the yellow soldermask because this PCB will be exposed and I’d like it to look more distinctive than the standard green. Because I’m going to be doing controlled impedance traces I needed to know the PCB dielectric constant and the thickness of the PCB copper and FR4 substrates. My emails to PCBWay were answered within 24 hours by Lynne and I got the answers I needed. The dielectric constant is 4.29 and the upper and lower layers are 18µm copper (note that since this was written they’ve now changed to 35µm).

I selected the China post option which means the standard 2-4 weeks went by before the boards arrived.


The boards look great to me. What’s really important is that they’ve managed to get complete and unbroken soldermask slivers between the tiny pads of the QFN.

The QFN is by far the most annoying and difficult part to reflow and frankly you need all the help you can get. Having soldermask there to prevent bridges will play a big part in my chances of a successful reflow.

If you look at the upper and lower right corners of the QFN you’ll see that I’ve used side-exit traces from the pads. Be careful when you do this because QFNs with a ground pad have that pad held in place by very small metal spurs that are exposed from the package at the end of the row of pins. If you don’t have solder mask covering your traces then there’s a small but real risk that you could accidentally ground a trace.

The drill positioning for the vias also looks great for a prototype service and the soldermask apertures appear to be well positioned with good tolerance. The silkscreen has the familiar dot-matrix low resolution style that you get with prototype services.

I’ve heard that the prototype services use different board houses for four layer manufacturing and that may account for the better quality results and higher price to go with it.

Here’s a shot of the back and front layer stack tell tales. I can see that they’ve got the stack correctly ordered so my controlled impedance traces will be correctly referenced to the ground plane.

Here’s my attempt at a spark gap. That’s an 8 mil gap between the two pointy ends. Do they work? I don’t know for sure but they’re free so I may as well include them. If you’ve never seen one before and don’t know what they do then there’s an entertaining video over at the eevblog that’ll tell you all that you need to know and plenty that you didn’t expect!

Building the board

To build the board I followed my usual process of tinning the pads, applying flux and then balancing the components on the tinned pads. The board is then carefully placed in my reflow oven and I execute a ‘leaded’ reflow profile. At around 180°C the little bumps of solder melt and the components sit down onto the pads.

The QFN is of course the biggest unknown in the process because of the ground pad that’s completely hidden from view. The ground pad must be connected and it must also have multiple via connections to a solid plane. The trick is getting just enough solder so that it will sit down onto the board at the same level as the surrounding pads and the solder must not wick away down the vias leaving you with nothing left on top. Of course you can’t visually check that you’ve got it right after the reflow, nor can you touch it up if you get it wrong.

If the pad is the device’s only connection to ground then you can test its continuity by probing the little supporting spurs that are exposed at the end of the rows of pads. I really dislike the QFN package but they’re here to stay so developing a repeatable assembly process is really important.

Here it is fully assembled and looking great in my opinion. I’ve opted to stick the HC-06 underneath the board with a double-sided sticky pad and connect it with wires to the 2.54mm header on the main board. It’s designed to stick out by about 20mm so that the bluetooth antenna is not obstructed.

The FPGA design

A block diagram of the FPGA design is shown above. It’s quite simple but does require some high speed processes. The flow of control works like this.

  1. The MCU programs a gate counter using the SPI interface.
  2. The FPGA receives a rising edge on its ‘enable’ input.
  3. Counting begins at the next rising edge of the sample input and ‘done’ goes low.
  4. When the reference counter reaches the gate limit counter then counting stops at the next rising edge of the sample clock.
  5. Done goes high.
  6. The MCU reads the two counters using the SPI interface.

It’s all quite straightforward and you can view the VHDL source code here on github. The important parts of the system are the 31-bit counters, and in particular the reference counter. These must be able to cope with counting at 200MHz. The choice of counter width is a trade off between counter speed and the maximum counter value. At 31 bits I can cope with up to 10 seconds of counting and hence the maximum gate time is 10 seconds.

A typical synchronous counter, of the type automatically created for you by the synthesiser when you use an addition-by-one expression, is not fast enough to count at 200MHz. To get to that sort of speed requires a faster implementation and I chose to reuse one available here at opencores.org.

The side effect of using a pipelined counter such as this is that counting takes more than one clock cycle. In this design that means that several clock cycles are used after the gate time has ended to flush the pipeline and let the counter settle. This can be seen in this ISIM simulation screenshot. Notice how the counters continue to update for a few clock cycles after the done_counting bit is set.




Pipelined counter operation (click for larger)

Xilinx ISIM… oh dear the bitrot really is starting to affect these venerable old tools. On Windows 7 ISIM was stable but clunky to use but now on Windows 10 it’s started to crash during certain common UI operations. Xilinx aren’t interested so it looks like the only solution if you need crash-free operation is to run Windows 7 in a VM.

To ensure that my timing goals are met I add timing constraints to the .ucf file:

TIMESPEC TS_ref_clk = PERIOD "ref_clk" 5 ns HIGH 50 %;

NET "inst_equal_precision_counter/ref_clk_div" TNM_NET = "inst_equal_precision_counter/ref_clk_div";
TIMESPEC TS_ref_clk_div = PERIOD "inst_equal_precision_counter/ref_clk_div" 10 ns HIGH 50 %;

NET "sample_clk" TNM_NET = "sample_clk";
TIMESPEC TS_sample_clk = PERIOD "sample_clk" 20 ns HIGH 50 %;

NET "spi_clk" TNM_NET = "spi_clk";
TIMESPEC TS_spi_clk = PERIOD "spi_clk" 20 ns HIGH 50 %;

Inspecting the verbose output from the post-PAR timing report shows whether I’ve managed to meet my timing constraints:

Timing constraint: TS_ref_clk = PERIOD TIMEGRP "ref_clk" 5 ns HIGH 50%;
For more information, see Period Analysis in the Timing Closure User Guide (UG612).

 120 paths analyzed, 120 endpoints analyzed, 0 failing endpoints
 0 timing errors detected. (0 setup errors, 0 hold errors, 0 component switching limit errors)
 Minimum period is   2.592ns.

All my constraints are met and this design can run well in excess of the 200MHz target. There’s no point in optimising any further once all constraints are met so it’s time to move on to the next part of the design.

The MCU and android programs

The MCU exists to program the FPGA and PLL and then continually service requests arriving over bluetooth from the app. Let’s take a look at the app design. Here’s the main display screen.

The display area is dominated by the measured frequency display at the top. The number of decimal places can be customised (none shown here) and I can opt to display the value in Hz, kHz or MHz.

The table in the center of the screen shows a number of statistics that are continually updated as samples are gathered.

The chart at the bottom shows how the measured frequency has changed over time and can be used to get a quick visual idea of the stability of the measured source.

The three numbers at the top right allow me to compare the measured frequency with the nominal value for this source and show the offset in ppm as well as the number of milliseconds per hour that a counter based on this frequency must be adjusted by in order to maintain accurate time.

Many settings can be customised, so let’s see the settings menu that’s accessible from the drop-down menu button at the top-right of the screen.

The first of the two available settings screens allows me to change aspects of the display. Changes to these settings are persisted locally on the android device. I can control how the frequency is formatted for display, what the ideal (nominal) frequency is and how many values the chart at the bottom of the screen should remember before resetting.

The settings on this screen control how the app interacts with the nanocounter device. I can change the bluetooth device ID, the number of seconds of measurement time (the gate time) as well as the reference frequency source and the filters applied by the two LTC6957 devices.

Changes to these settings are persisted locally on the android device as well as transmitted to the MCU when they are changed and when the app starts up so that the MCU can apply them accordingly.

Calibration of the on-board reference clock is important because a TCXO has a temperature variance, albeit a much smaller one than a standard crystal, and will drift over time as it ages. The app allows me to capture a temperature offset and the current ambient temperature.

Calibration settings are written to a flash page on the MCU and a history of up to 112 settings can be stored. When the app starts up it reads the calibration array from the MCU and applies the most recent one to the session. Calibration settings are not applied when the external reference clock is selected.

The source code for the android app is available on github. Feel free to take a look around. The app is also available on the Google Play Store. Searching for ‘Nanocounter’ should find it.

The MCU program

The MCU program uses my stm32plus library to communicate with the peripherals. There’s no direct support in the library yet for the 072 series but I’m only using peripherals that are also present on the 051 which is supported so I build against the 051 and use adjusted startup and linker scripts to cater for the larger memory sizes in the 072.

The program is mostly asynchronous but there is a main loop in Program.h that performs some work.

  for(;;) {

    // check the command processor for activity

    _commandProcessor.run();

    // if the lock is lost then stop

    if(!_pllLockState && _frequencyCounter.isStarted())
      _frequencyCounter.stop();

    // if not started, locked and counters have been processed then start

    if(_pllLockState && !_frequencyCounter.isStarted() && !_countersReady)
      _frequencyCounter.start();

    // new counters available?

    if(_countersReady) {

      MillisecondTimer::delay(10);

      // read from the FPGA

      _frequencyCounter.readCounters(counters);

      // stop (reset) the counters inside the FPGA

      _frequencyCounter.stop();

      // set the latest counters inside the command processor

      _commandProcessor.setCounters(counters);

      // reset for a new run

      _countersReady=false;
    }
  }

The command processor run() method simply keeps the ‘link’ LED lit if there has been any bluetooth activity in the last second. Bluetooth commands are received and processed completely asynchronously.

A USART DMA channel is used to receive the command from the app and then I get an interrupt to tell me it’s ready. I then process the command in the IRQ context and initiate a USART DMA tranmission to send the response back to the app.

The device cannot function unless the PLL is locked so there’s an emergency stop feature if the lock is lost. The ‘lock’ LED is also lit up as long as the PLL is locked. I don’t need to poll for this because I’m using the STM32 EXTI feature to get an interrupt when the state of the PLL lock GPIO pin changes.

The rest of the loop just goes around in a loop starting a counting session on the FPGA, checking if it’s done (another EXTI interrupt) and if so then reading the final counters back and making them available to the app.

All the source code is of course free and available here on github.

Testing

Oh this was a tale of ups and downs. After assembly the first thing that I did was attach the ST-Link/v2 programmer and check if the MCU was alive. It was. Check that one off the list.

I then set about writing the code that loads the FPGA bitstream into the FPGA. Success again. I’m on a roll here.

Next I need to program the PLL so I wrote the SPI code to load the register values to it. The registers loaded OK but oh no the PLL doesn’t lock. Not good. Not good at all. The AD9553 has some basic debugging features built in to it that allow you to route some of the internal clocks to the ‘lock’ pin and that can help you determine where the fault lies.

The only clock that gave me anything at all was the PFD reference input. At least that showed that the input clock multipliers and dividers were working. The device wasn’t totally broken. I put the board under the microscope and inspected the pads along the side of the QFN. Everything looked good. Suspicion naturally fell on the ground pad because I can’t see it to check it. All I could tell was that the solder bump on the ground pad had definitely melted because the QFN had sat down flush to the board. But what if the solder had wicked down the vias? It didn’t look like it from below. I spent hours fiddling with the debug settings in the AD9553 and was just about ready to give up when I posted a question detailing the symptoms in the ADI forums.


Something wasn’t right in there

Programming the AD9553 involves writing the registers, commiting them to the device and then calibrating it, after which you should get a lock. An engineer replied that the order of the calibration commands that I was issuing didn’t look right and suggested a fix. It worked! I got a PLL lock and have never failed to get a lock ever since. I can’t thank the ADI engineers enough that they take the time to answer hobbyist questions on their forums.

Now it’s time to fire up the Android app and check out the entire thing, front to back. There’s actually a gap of a couple of months here as I wrote the app over the course of some free evenings and weekends.


The app running on my cheapo £35 7″ tablet

Here we can see the app measuring the performance of a 16MHz crystal on board a no-name Arduino Uno board that I got on ebay for a couple of pounds. This sort of board is going to be using the absolute cheapest components that they can source so I wasn’t expecting great performance from the crystal. I was actually quite surprised to find that it’s running at only 31ppm off the nominal frequency. You can see from the panel at the top right of the display that I would need to correct a clock based on a millisecond counter within the MCU by subtracing 111ms from the count each hour.

Calibration

Calibrating the device requires me to use an external frequency standard to measure the actual performance of the onboard TCXO. The easiest way to do this is to connect up the frequency standard to the sample input port.


Measuring the onboard TCXO with the app running on my phone

The frequency standard that I’m using is a GPSDO with a 10MHz output. GPSDOs are very interesting devices and I’ll be publishing reviews and videos on the one that I’ve got in a future article.

Because the frequency standard is known to be as near-as-who-cares to 10MHz the value that you see on the display is the performance of the onboard oscillator. If we rearrange the sample frequency equation given at the start of this article so that it yields the reference frequency before the x20 factor of the PLL we have:

Comparing the two equations shows that the frequency of the onboard TCXO is 10MHz+(10MHz-displayed value). In other words, it’s 10,000,004Hz at my current room temperature of 21.6°C.

To see how that value of 379ppb compares to the specifications produced by Connor Winfield we need to refer to the datasheet.

Unfortunately I can’t do an accurate calculation because all the reference figures are given at 25°C but I can see that my figure of 379ppb is well within the ±1ppm factory calibration range.

Watch the video

I’ve cobbled together a video in which you can see me using Nanocounter in various different scenarios. I’ll measure the frequency of a couple of crystals and then hook up my GPSDO frequency standard for comparison.

Get the design files

If you’d like to modify the design files, or just want to upload the Gerbers to a PCB printing service then you can download them in a zip file from my downloads page. Click on the Hardware schematics and CAM files tab at the top of the page.

Final words

I hope that you’ve enjoyed reading this project as much as I’ve enjoyed building it. If you’ve got anything to say then please feel free to leave comments down below or use the forum to start a longer conversation.


Directly driving a 7-segment LED display with the STM32

$
0
0

Seven segment LEDs are an extremely cost effective way to add a large, bright and very readable numeric display to your project.

Displays similar to the one pictured above can be had for as little as 50 cents each on ebay in the common heights of 0.56″, 0.36″ and 0.28″. You can choose anywhere between one and four digits in the same package. They’re referred to as seven segment but really they’re eight because each digit comes with a little decimal point down at the bottom right.

Configuration

The multiple digit packages utilise a wiring configuration designed to minimise the number of pins required to drive it without having to embed any logic at all within the package.

If you count the number of segments on, for example, a three digit display you’d quickly realise that a simple configuration that exposed each LED on its own dedicated pin would require (8 * 3) + 1 = 25 pins on the package, of which you would need to attach 24 to your MCU to drive it. That’s far too many and is the reason why they come in common cathode or common anode configurations.

Common cathode configuration

Let’s look at common cathode first.

In this configuration there are dedicated power pins for each of the 7 segments but the same segment on each digit are all connected together. On the other side of the LED you can see that all eight cathodes for a digit are tied together and presented at a single pin.

If you take a moment to digest this you can see how we can light up a segment of our choosing on any digit. For example, to light up segment A on digit two we would apply a current to pin 11 while grounding pin 9. Pins 8 and 12 must be disconnected or otherwise prevented from allowing current flow.

To light segment A on digit 1 we would disconnect pin 9 and ground pin 12, and finally for digit 3 we would disconnect pin 12 and ground pin 8.

Multiplexing

Now you should be getting an idea of how these displays are intended to be driven. Let’s look at a fully worked example of how we would display the number “123”.

Firstly we would light segments E, F on digit 1 by enabling current flow through pins 1, 10 and 12. Then we would light segments A, B, D, E, G on digit 2 by enabling pins 11, 7, 2, 1, 5 and 9. Finally we would light segments A, B, C, D, G by enabling only pins 11, 7, 4, 2, 5 and 8.

If we repeat the above actions fast enough then the human eye will perceive all three digits to be constantly lit even though we are switching them on and off very quickly.

Common anode configuration

This article is going to focus on the common cathode type of display but for completeness I’ll show you the other configuration, just so you know that two incompatible types are available.

In the common anode configuration we again have separate pins for each segment and again all equal segments on all digits are wired together but this time the cathode ends of the segment LEDs are individually exposed and it’s the anodes that are all connected together on each digit.

The multiplexed driving technique is exactly the same for common anode displays but that doesn’t mean you could use common anode where a design calls for common cathode because you can’t, you would have to change the design.

Driving with an MCU

There are a few options available if you have an MCU and you want to drive one of these displays. If you have an MCU with a limited number of IO pins, such as an Arduino Uno then your best option is to use a dedicated driver IC that will do the work for you.

The Maxim MAX7221 will drive common cathode displays of up to a whopping eight digits while requiring just a three wire SPI interface to the host MCU. Using just one of these ICs you could have two of the biggest four digit displays in your project at a cost of just three MCU pins. I’ve used this IC many times before in projects that you can read about on this site. The main drawback of this IC is that it requires a 5V supply and 5V levels at the SPI pins. This is no problem for the Arduino Uno but it means it can’t be used with an STM32 without a level shifter.

If you’re using an MCU with a large number of GPIOs, such as most of the STM32 packages, then you have the option of driving these displays directly for the cost of just eight resistors and three n-channel MOSFETs, and that’s the method that we’re going to explore here today.

Direct drive circuit

Here’s the circuit diagram that I use to drive a three digit display that has blue LEDs. It’s a snapshot from a much larger circuit that I’m working on.

The choice of resistor is important because it limits the amount of current flow and sets the overall brightness of the display. I’ll be using the STM32 F0 discovery board that hosts an STM32F051 MCU to implement this circuit.

The first thing that I need to do is find the MCU datasheet and determine the maximum current that the device can source and sink.

Those limits make reference to another table earlier in the datasheet that tells us the total current source and sink for all pins.

So we have a per-pin absolute limit of 20mA and an overall device limit of 120mA. To avoid heat buildup and allow the device to actually do other work as well we will stay far away from those limits.

Are there any other limits? Yes there are. It pays to read the entire datasheet because hidden away in a footnote there is a very important limitation regarding GPIOs PC13 to PC15.

We will not use these pins.

Resistor calculation

To calculate the resistor values we need to know the forward voltage of the LEDs in the display. This is easily tested by using your multimeter in its diode testing mode.

The meter shows a forward voltage of about 2.6V which is average for a blue LED. Now I’ll take a wild guess that because modern LEDs are very bright at low currents then 2mA will be sufficient current to get a nice, readable brightness. To match the STM32 F0 Discovery board I’ll test this with a 3.0V supply. That means a resistor of (3.0 – 2.6) / 0.002 = 200Ω is required.

LEDs don’t photograph well so please take my word for it that this is nice and bright. Can the STM32 handle it? 2mA falls well below the per-pin limit and the worst case scenario is going to be all eight segments lit at the same time giving a total current source of 8 * 2 = 16mA. No problem at all. The package shouldn’t even get warm.

The problem with using the 200Ω resistor that we calculated is that each digit is only lit for 33% of the time which will make it appear three times as dim as we are expecting. Therefore we need to lower the resistor by a factor of 3 and use a value of 68Ω instead.

This will raise the peak current seen by the LED to 6mA but the average current will still be 2mA. In the worst-case scenario where your MCU hangs or crashes while driving all eight segments of a digit then it will be sourcing 8 * 6mA = 48mA. This is still within safe levels and will not burn up the package.

This figure of 48mA is the reason for each digit pin being switched on or off using a MOSFET. If we were to directly connect these pins to the MCU then we would be in danger of sinking 48mA into a single pin which would probably permanently damage it.

The resistors, MOSFETs and jumper wires are all in place and we are ready to develop the firmware. My project circuit specifies the Vishay SI2374DS MOSFET which is a surface mount device. For this test I am using the through-hole BS170 instead. The choice of n-channel MOSFET is not important but for efficiencies sake you should choose one with a low on-state drain-to-source resistance. Less than 1Ω is easily found.

Firmware

I chose to implement the firmware as an example project within my stm32plus library. The concepts are simple so you should have no issues porting it to whatever framework suits your project. The firmware is implemented in a single file that you can view here on Github.

The design works by using Timer 1 to generate interrupts at a frequency of 180Hz. Each time the interrupt fires we turn off the digit that we were last displaying and move on to setting the GPIOs necessary to light the next digit. Therefore each digit flickers rapidly at 180/3 = 60Hz, a figure I selected to match the refresh rate most commonly used by PC monitors. This gives a display that appears stable to the human eye.

Here’s a breakdown of the important parts of the firmware.

static const uint8_t AsciiTable[]= {
  0,  // SPACE
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  // skip
  0b11111100,  // 0
  0b01100000,  // 1
  0b11011010,  // 2
  0b11110010,  // 3
  0b01100110,  // 4
  0b10110110,  // 5
  0b10111110,  // 6
  0b11100000,  // 7
  0b11111110,  // 8
  0b11110110   // 9
};

We want to allow the controller to display ASCII text strings so we need a table to convert ASCII to a bitmap of which segments should light up for that character. Printable ASCII starts at 32 (space) so we start our table there.

Each entry in the table is a single byte with one bit per lit-up segment in the format ABCDEFG0. Unused ASCII codes are set to zero. In this example I only need the digits 0-9 so that’s all there is in there. You can easily see how to extend this.

enum {
  SEGA = 0,   // PA0
  SEGB = 3,   // PA3
  SEGC = 8,   // PB8
  SEGD = 4,   // PB4
  SEGE = 3,   // PB3
  SEGF = 1,   // PA1
  SEGG = 2,   // PA2
  SEGP = 5,   // PB5
  DIG1 = 9,   // PB9
  DIG2 = 2,   // PB2
  DIG3 = 10   // PB10
};

The pins used for each GPIO are stored in an enum for easy reference. The seemingly random assignment matches a project I’m currently working on and also shows that the pin placement is completely flexible.

GpioA<DefaultDigitalOutputFeature<SEGA,SEGB,SEGF,SEGG>> pa;
GpioB<DefaultDigitalOutputFeature<DIG1,DIG2,DIG3,SEGC,SEGD,SEGE,SEGP>> pb;

All pins are initialised as outputs. To light a segment I will set the segment output and the corresponding digit MOSFET gate output HIGH. Current will flow from the segment output GPIO, through the LED and the MOSFET and the LED will light. To switch a digit off I simply switch off its MOSFET.

/*
 * Initialise timer1 running from the high speed internal APB2 (APB on the F0)
 * clock with an interrupt feature
 */

Timer1<
  Timer1InternalClockFeature,       // the timer clock source is APB2 (APB on the F0)
  Timer1InterruptFeature            // gain access to interrupt functionality
> timer;

/*
 * Set ourselves up as a subscriber for interrupts raised by the timer class.
 */

timer.TimerInterruptEventSender.insertSubscriber(
    TimerInterruptEventSourceSlot::bind(this,&Timer7SegmentTest::onInterrupt)
  );


/*
 * Set an up-down-timer up to tick at 80kHz with an auto-reload value of 444
 * The timer will count from 0 to 444 inclusive, raise an Update interrupt and
 * then go backwards back down to 0 where it'll raise another Update interrupt
 * and start again. Each journey from one end to the other takes 1/180 second.
 */

timer.setTimeBaseByFrequency(80000,444,TIM_CounterMode_CenterAligned3);

/*
 * Enable just the Update interrupt, clearing any spurious pending flag first
 */

timer.clearPendingInterruptsFlag(TIM_IT_Update);
timer.enableInterrupts(TIM_IT_Update);

/*
 * Start the timer
 */

timer.enablePeripheral();

Setting up the timer in stm32plus is a simple task of declaring it with the clock and interrupt feature, inserting ourselves as a subscriber to the interrupts, setting the desired frequency and then enabling the peripheral.

int value = -1;

for(;;) {

  value++;

  if(value>999)
    value = 0;

  // translate value to ascii, left justified

  _display[0]=_display[1]=_display[2]=0;
  StringUtil::itoa(value, const_cast<char *>( _display), 10);

  // wait for 100ms

  MillisecondTimer::delay(100);
}

The example code then goes into an infinite loop counting up from zero to 999 and then wrapping around and starting again.

/*
 * Subscriber callback function. This is called when the update interrupt that we've
 * enabled is fired.
 */

void onInterrupt(TimerEventType tet,uint8_t /* timerNumber */) {

  // verify our expectation

  if(tet!=TimerEventType::EVENT_UPDATE)
    return;

  // turn off the last digit we displayed. This needs to be done first to avoid
  // switched off segments becoming faintly visible during multiplexing

  _digits[_currentDigit].reset();

  // advance to the digit we just set up

  if(_currentDigit>=2) {
    _currentDigit=0;
    _currentDigitPtr=_display;
  }
  else
    _currentDigit++;

  // get the character to display at this position

  uint8_t c=*_currentDigitPtr++;

  // check the bottom end of the range

  if(c<=' ')
    c=' ';

  // get the segment state bitmap from the table

  uint8_t bits=AsciiTable[c-' '];

  // for each bit in the map, set/reset the correct state in the segments

  for(uint8_t j=0;j<7;j++) {
    bool state=(bits & 0x80)!=0;
    _segments[j].setState(state);
    bits <<= 1;
  }

  // process the decimal point if there is one

  if(*_currentDigitPtr=='.') {
    _segments[7].set();
    _currentDigitPtr++;
  }
  else
    _segments[7].reset();

  // switch on the digit we have set up

  _digits[_currentDigit].set();

  // we'll be back in 1/180s which means we are displaying each digit at 60Hz
}

This is the interrupt handler where the real work happens. We switch off the previous digit before setting up the seven segments that form the next digit. We then explicitly check to see if we need to turn on the decimal point before finally lighting up the next digit.

You’ll need to view the entire file to see the types of the member variables that are used but you should be able to understand the logic flow from this snippet.

Here’s a photograph of the display in action. It works as expected and the display is a comfortable and even brightness with no artifacts or flickering observed.

Adapting this technique for your project

If you want to use this technique in your own project then you should follow the same procedure that I did. To summarise:

  1. Count up the pins you’ll need and verify you have enough available on your MCU.
  2. Measure the forward voltage of your LED segments and experiment to find a low current level that gives a usable brightness.
  3. Calculate a resistor that limits the LED current to your selected value and then divide it by the number of digits on your display.
  4. Verify that your MCU can source the current you will draw, taking into account the worst case scenario where the MCU hangs and a digit is permanently on with all segments lit.
  5. Select an n-channel MOSFET with a low drain-source resistance (less than 1Ω is easily achievable) and check that the on-state power dissipation is well below the maximum the package can support.

If you need any help with driving these displays then please feel free to contact me or leave a message down below in the comments.

A development board for an STM32G081 MCU

$
0
0

I’ve been an avid user of ST’s F0 series ever since it was launched. The 48MHz Cortex M0 is almost always the perfect MCU for every project that I tend to build and it’s so easy to program and debug that, for me, it’s the default answer to ‘which MCU should I use for this project?’ So when I noticed that ST had launched a ‘G0’ range I just had to have a closer look.

What’s the difference?

In short, there’s a Cortex M0+ core at the heart of the G0 series instead of the M0 that’s in the F0. To find out the difference between the M0 and the plus we have to visit ARM’s website.


Cortex M0+ block diagram

There’s a slight increase in performance and an optional Memory Protection Unit (MPU) that the RTOS guys may get excited about but really there’s not much else in the way of additional features.

The headline claim made by ARM is a further decrease in the already class-leading power consumption and a scarcely concealed attack on the remaining market for 8-bit MCUs. ARM really want you to choose this core for projects that might previously have used an 8-bit device.

A shorter pipeline

One interesting way that they’ve managed to decrease power consumption is by reducing the number of clocks required for the instruction pipeline from three to two by squashing the instruction decode stage across the two clocks and reducing the need for power-expensive access to flash memory.


Pipeline clocks and stages

I have to say that I’m not convinced that this is a good thing for performance. The marketing blurb correctly states that a shorter pipeline increases responsiveness but we engineers will immediately point out that faster response means lower throughput. High-end devices such as the Intel i7 have more than 20 pipeline stages and even ARM’s own Cortex-M7 has 6 stages.

However, ST have given us more features that I think will make the G0 series the default choice for my future designs.

F0 vs G0

Clock speed. You can’t fail to notice that the maximum clock speed for the G0 is 64MHz compared to 48MHz for the F0. The cynic in me says that this is compensation for the performance hit incurred by shorter instruction pipeline.

Flash memory. The F0 goes up to 256k. ST has plans for the G0 to go up to 512k but so far they’ve only released the G071 and G081 that go up 128kb.

SRAM. The F0 goes up to 32k. The G0 improves slightly on that with 36k. More SRAM is always good so that’s a welcome improvement.

Price. They’re about the same. If I compare the STM32F091RBT6 to the similarly spec’d STM32G081RBT6 then they come about about the same with the G0 edging it with the 36k SRAM and 64MHz maximum clock.

ST’s development boards

At the time of writing ST are offering two development boards. There’s the low cost G071 Nucleo that features a degree of Arduino compatibility though goodness knows why they bother with that.


The G071 nucleo board

At just $10 the Nucleo is really cheap and a great way to kick the tyres of the G0 series if you want to get started really quickly.

The other board on offer is a full-featured eval board featuring the STM32G081RBT6 MCU and costing an eye-watering $400.


The G081 eval board

To be fair, for that $400 you do get a ton of peripherals to play with. If your employer is paying then this might be the board you want to buy.

I almost bought a Nucleo board but then reconsidered and decided that the best way for me to learn about the ins and outs of the G0 would be to build my own development board. So that’s what I did.

My development board design

I decided that I’d go for the following features on my board.

  • STM32G081RBT6 MCU with 128k/36k flash/SRAM in the LQFP-64 package.
  • All GPIO pins to be available on top pin headers, grouped by port and ordered by pin.
  • All pins also to be available in breadboard-friendly 100mil spaced headers on the bottom of the board.
  • Three selectable power supplies: 3.3v, 2.5v and 1.8v.
  • Three user LEDs routed to ports that have timer outputs available so flash/fade effects can be protyped.
  • A third-party USB-to-serial IC will connect an MCU USART to the outside world through a mini-USB port.
  • External 8MHz HSE oscillator so that the USB-to-serial IC can be used at high speeds.
  • External LSE 32.768kHz oscillator for RTC use with backup coin-cell battery.

The STM32G081RBT6 was selected because it’s currently the biggest one that ST have released and it always helps to have the most resources available on your dev board.


The 64-pin G081 IC

When it comes to transferring your design from the dev board to a custom PCB then that’s when you can downsize to a device optimised for your resource usage.

I decided right away that the pin headers would expose every GPIO port and pin group and the headers would be grouped by port and ordered by pin. I’m seriously bored with hunting for pins on development boards that have been ordered to facilitate easy PCB routing!

Pin headers will be provided on the top and bottom of the board, just like the ST Discovery boards except mine will have pins on top that are long enough to actually accept a jumper wire without them falling off because they’re too loose.

A selectable power supply has always been something I’ve wanted from a development board. If my project is standalone then I’ll probably run it at 3.3v but there are times when I need something else. For example interfacing with an FPGA might require 2.5v and a battery-powered design might be spec’d to run at 1.8v to optimise power consumption.

A standalone IOT device might be required to keep accurate time so I decided to include a 32.768kHz LSE oscillator and the associated backup battery. This will be a 3v CR1220 coin-cell.

It’s quite common for an MCU to have to connect back to something more powerful such as a PC or Raspberry PI so I’ve included a USB-to-serial device – the cheapo CH340E – on my board. To enable driving the CH340E at high data rates I’ve included an 8MHz HSE oscillator so that the MCU peripheral clocks will be as accurate as they can be.

The CH340 saga

I’ll start with a confession. This is my second attempt at building this board. The first attempt was largely the same as the second except that first time I used the CH340G instead of the CH340E. And the CH340G didn’t work.

The CH340/341 series are a family of USB to serial converters that fulfil the same role as the much more well known FTDI chip that you can find on official Arduino boards. The CH340 is made by a Chinese company, WCH, and is much cheaper than the FTDI chip. I bought my CH340’s for about £3 for 10 on Ali Express. The internet says good things about the CH340 so I felt confident.

The CH340 is actually a series of ICs that come in different packages that provide different levels of functionality. The datasheet is all in Chinese but has been translated to English and can be found here.


All the CH340s (so far)

The 340G is the first one that I bought, and it arrived in packaging that I would not describe as confidence-inspiring.


Loose packed ICs

Nevertheless I made a board with it and selected a good quality Abracon surface mount 12MHz oscillator to support it. Unfortunately it would not enumerate as a USB device. I think it was having trouble getting the oscillator to start as I couldn’t detect a signal coming from it and I’m fairly sure my probes weren’t loading the crystal down as I could probe another, similar oscillator on the board. On power up I could see short spurious waveforms come from the oscillator but they never stabilised and the output always went flat.

I tried higher load-capacitors, then lower. Then a new CH340G. Nothing worked and I was out of options so I decided to cut my losses, and my time, and replace the CH340G with the CH340E that has a built-in oscillator and requires only decoupling capacitors to use it. I could of course have switched to an FTDI but that would be giving up and I wasn’t ready to do that. Unfortunately the IC package was different so I also had to redesign that part of the board.

Three weeks later and the new boards arrived and so have the new ICs, again from Ali Express. This time though the packaging was much more like they’d actually come from a production reel and not some trader’s back pocket.


Proper IC packaging

We’ll see how I get on later in this article, but now let’s have a look at the schematic for the development board.

Schematic


Schematic image

Click on the full image to see a PDF of the schematic. It’s quite a modular design so let’s break it down into its parts.

The power supplies and the USB interface

I wanted to have a choice here and linear regulators are cheap so I designed in three of them to give power supply options of 3.3v, 2.5v and 1.8v. A jumper is provided to select the desired level. Since the power input comes from the potentially very noisy 5V USB line I’ve included my usual filter network consisting of a ferrite bead and some capacitors.

The USB interface is provided by a mini-B connector. I only ever use the mini-B or the full size B connector on my boards because they’re the most reliable. The micro-B connector used by most smartphones before they started switching to type-C is too delicate and easy to tear off the board. The on-board components are protected from static by the USBLC6 IC that I include as a matter of course with every USB design.

The CH340E can only run at 3.3v and can be enabled with the P2 jumper. D1 ensures that when 2.5v or 1.8v are selected then that level does end up being routed to the VCC pin of the CH340. The voltage drop of that same diode also has the effect of making VDD for the MCU actually about 3.0 to 3.1v instead of 3.3v. Something to be aware of.

The CH340E only requires a pair of decoupling capacitors to run at 3.3v, one on the VCC pin and the other on the V3 pin. The use of the diode D1 actually means that the CH340E will be powered at 3.3v and the signal levels will be about 3.0v but that’s within spec so I don’t expect any issues.

The STM32G081RBT6 MCU

Here’s the heart of the system, the MCU. All the GPIOs are broken out to pin headers Except those that have crystals attached, though I don’t think those are any great loss to a designer.

External 8MHz and 32.768kHz surface mount crystal oscillators are provided as the HSE and LSE clock sources. There’s a reset button and the SWD programming interface is broken out to the ungainly 20 pin ARM connector that connects directly to my ST-Link programmer via the ribbon cable.

Three LEDs are provided that source current from the 3.3v supply regardless of the selected core voltage so that the brightness won’t vary — and in the case of the blue LED so it will actually light at all — when the supply is 1.8v.

ST have optimised the pins on the G0 series so that you get more GPIOs. In their older chips you’d find a pair of VDD/GND pins on each side of the package which I think tells you a bit about the internal floor planning of the device and how things are arranged internally on the die. This G0, however has a single VDD/VDDA pin and a single GND pin. VREF is there but they’ve done the decent thing and placed it right next to VDD/VDDA so you can tie them together and right next to those is VBAT, which obviously has to be separate.

I’ve designed in a coin-cell holder for a CR1220 3v backup battery, protected from reverse-current by a schottky diode with a low forward voltage. According to ST’s datasheet the worst-case current consumption for VBAT when VDD = 3.0v is 470nA. A quick look at Energizer’s datasheet shows that you can expect to get about 30mAh from it before the voltage starts to drop off. 0.00047mA into 30mAh gives 63829 hours or 7 years. I’m sure there are other losses and inefficiences to consider but still, that’s a long time.

IO connectors

They’re all there with the exception of those used by oscillators. One of the great things about ST’s package redesign is that there are now more GPIO pins available. GPIO ports A, B and C are fully available although in my design PC14 and PC15 are taken by the 32.768kHz LSE. This is great for designs that might need to bit-bang a parallel bus. GPIO port D has pins 0 to 9 available.

Bill of materials

Here’s the full bill of materials for this project.

IdentifierValueQuantityDescriptionFootprintComment
B11CR1220 holderCustomavailable on ebay
C122µ1Polarized Capacitor (Radial)CAPPR2-5x11
C2100µ1Polarized Capacitor (Radial)CAPPR2-5x11
C310n1Capacitor0603
C4, C12, C13, C17, C20, C23100n6Capacitor0603
C5, C224.7µ2Capacitor0805
C6, C7, C8, C9, C10, C11, C217Capacitor0603
C144.7n 250v1Capacitor0805
C15, C1622p2Capacitor0603
C18, C1910p2Capacitor0603
D1, D5STPS0520Z2Schottky RectifierSOD123
D21LED2012RED
D31LED2012YELLOW
D41LED2012BLUE
FB1BLM18PG221SN1D1Ferrite bead0603
P11Header, 3-Pin, Dual rowHDR2X3POWER
P21Header, 2-PinHDR1X2CH340 EN
P31USB Mini BUSB Connector
P4, P52Header, 16-PinHDR1X16GPIOA
P6, P72Header, 14-PinHDR1X14GPIOC
P8, P112Header, 10-PinHDR1X10POWER
P9, P122Header, 10-PinHDR1X10GPIOD
P101Header, 2-Pin, Dual rowHDR2X2BOOT
P13, P142Header, 16-PinHDR1X16GPIOB
P151Header, 10-Pin, Dual rowHDR2X10SWD
Q1, Q2, Q3SI2374DS3MOSFET-NSOT23-3N
R1, R33302Resistor0805
R2, R410k2Resistor0805
R51501Resistor0805
R6, R7, R8100k3Resistor0805
R91M1Resistor0805
SW11ButtonPCB ButtonRESET button
U1MCP1700T-3302E/MB1SOT-89-MB3_NLDO, 3-Pin SOT-89
U2MCP1700T-2502E/MB1SOT-89-MB3_NLDO, 3-Pin SOT-89
U3MCP1700T-1802E/MB1SOT-89-MB3_NLDO, 3-Pin SOT-89
U4USBLC6-2SC61SOT23-6_L
U5CH340E1MSOP-UN10_N
U6STM32G081RBT61ARM Cortex-M0+STM-LQFP64_L
Y1ABRACON ABM3B18MHz crystalCustom
Y2Epson FC-135132.768kHz crystal2012

PCB layout


3D view of the board

The physical layout of the board is optimised for ease of use. I’m tired of boards that lazily route the GPIOs to the headers based on where they are on the IC package making me hunt up and down the tiny and often faded silkscreen legend for the right pin. On this board the GPIOs are grouped by port and ordered by pin number. It only takes a few extra seconds of routing time, even with ST’s knack of placing a port’s pins at seemingly random locations around the package.

I’ve provided rows of downward and upward facing pins for each port. The downward facing pins are designed to be the standard 100 mil apart all down the board and the rows on the opposite side of the board are a multiple of 100 mil away so that the board will mate with a breadboard.

The upward-facing pins are designed to accept the usual jumper wire-interconnects that we use so often.

Overall the board was easily routed on two layers with a ground plane top and bottom and is a little larger than ST’s discovery boards. I even had space for four M3 screw holes if the board ever needs to be permanently mounted into a chassis.

I uploaded the boards to JLPCB in China and paid the ridiculously low fee of $2 for a pack of 5. The lowest price shipping option added about $7 shipping to the price. Delivery took about 3 weeks.

Building the board



Top and bottom view of the bare board

The boards arrived and are looking great. I was particularly pleased to see that JLPCB have ignored the solder mask expansion around the pads. Solder mask expansion is the gap between the edge of a pad and where solder mask starts and is used to counter inaccurate placement of the solder mask in the manufacturing process.


Accurate solder mask placement

I set a rule of 4 mil which would leave a tiny solder mask sliver of about 1 mil between the MCU pins. It appears that JLPCB have enough confidence in their process to ignore the expansion rules and place the mask right up to the pins. The benefit of this is that I’ve got, as you can see above, a complete mask between the 0.5mm pitch pins of the MCU and that will greatly reduce the chance of accidental solder bridges.

My process for building a board is quite slow but I find it reliable. I first tin the pads with solder, then I reapply flux to the pads, then I place the surface mount components on the tinned pads and then I reflow the board in my halogen reflow oven. When the reflow is complete I touch up any problems manually and then solder in the through-hole components by hand. Finally I wash the board using hot soapy water and a toothbrush, rinse it off with cold water and leave it out to dry for at least a day.


All built and ready for testing

The reflow went OK this time. The only issue was that one of the 0603 capacitors got blown off its pads by the oven fan before it could reflow. I fixed that manually with my hot air gun.

One interesting thing I noticed while inspecting the board under my microscope was the design of the 32.768kHz crystal package.


It took a 2x macro and many megapixels to shoot this

It appears that the designers wanted to show off the internals so they fitted a clear plastic window into the top of the 1206 package so you can see how it works. I have no idea how crystals are constructed but I do find it fascinating and it looks a bit like a tuning fork. Does anyone out there know what’s going on in there?

Testing the board


ST-Link hooked up to the board

With the first revision of this board giving me so many CH340-related problems I was naturally a little apprehensive when connecting this for the first time.


USB View looking good

I need not have worried, the CH340E enumerated first time, showing up immediately there in the Windows USB View utility. I could proceed with firmware testing.

I decided that the quickest way to test out the board would be to use ST’s STM32CubeMX software to auto-generate some code that would set up USART2 to talk to the CH340E and also the three pins that are connected to the LEDs.


CubeMX pin configuration

I decided to enable RTS and CTS for USART operation even though I was going to operate in asynchronous mode for this test. I clicked through the CubeMX screens to enable the core clock at 64MHz using the external HSE as a clock source and then got it to generate a project for the free System Workbench for STM32 IDE. I’m pleased to see that this is yet another Eclipse-based IDE and I’ve been an Eclipse user for more than 15 years so I immediately feel at home and ready to code.

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2019 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

Auto-generated code

ST’s auto-generated code isn’t too bad at all. I’ve seen all types of auto-generated code that range in quality from ‘that compiles?’ to some that really look like a human wrote it. ST’s code contains comments that mark out where you can place your code without fear of it being overwitten by re-runs of CubeMX when you need to change your design. That’s great but my advice would be to keep your modifications of auto-generated files to a minimum and always check in or stage your changes before you re-run CubeMX.

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
    HAL_Delay(200);
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_14);
    HAL_Delay(200);
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_15);
    HAL_Delay(200);

    HAL_UART_Transmit(&huart2, (uint8_t *) "hello world\r\n", 13, 1000);
    HAL_Delay(400);
  }
  /* USER CODE END 3 */

My small additions to test the firmware

I added a few lines of test code to write out Hello World every second to the USART at 115200kbps and also cycle the LEDs. Compilation was immediately successful and the firmware was uploaded through the IDE’s connection to ST-Link via OpenOCD.

That CH340 again…

The firmware started running and the LEDs were cycling as expected but Windows was now having problems detecting the CH340E. What the? It was working before. Had I broken it? I fired up USB View and found it there with a different vendor ID than it had before.


USB View showing the unexpected VID

When I first plugged in the board it had a VID of 1A86 which matches the VID in the Windows driver package that I downloaded. Now it’s got a VID of 9986 that doesn’t match anything in the driver. Weird.

My first reaction was to modify the driver package to include 9986 in the .INF file and re-install it. This worked so I was half pleased and half uneasy about what had actually happened. I decided to do some more research and it turns out that if the RTS pin ‘has too much load resistance’ at boot time then the CH340E will enumerate with a VID of 9986. This behaviour is undocumented (thanks for that) and my guess is that it’s a bootstrap configuration feature designed for a specific customer.

One solution is to not configure RTS if you’re not going to use it, or if you are then wait until USB enumeration is complete before you configure the pin and keep your fingers crossed that the CH340E doesn’t reset independently of the MCU. Another solution is to configure your drivers to accept both possible VIDs. That’s what I’ve done with Windows.

With my VID driver hack still in place I fired up Realterm, my favoured serial port test utility and configured it with the correct virtual port number and configuration. It immediately started ticking away with the expected Hello World message every second.


Realterm receiving data from the board

That’s a relief. Now I’m pleased that it all seems to work and I can go on to develop actual projects using the STM32 G0 series confident in the knowledge that they’ll work as expected.

Free gerber files

If you’d like to build this board yourself then you can download the Gerber files from here. They should be accepted by your favourite online PCB manufacturer.

Video and conclusion

If you’d like to watch a video of me faffing about with the PCB, explaining badly how it works and then stumbling through the firmware testing then it’s here on YouTube.

Embedding a video in the webpage looks cool but really you want to go to YouTube to watch it for the full high definition experience.

If you’d like to comment on this article then please leave a comment below or click here to visit the forum thread that I’ve started if you’d like to have a more lengthy conversation.

A USB microphone for online meetings

$
0
0

Here in the UK the new reality of working in the IT business over the past year has been that we’re all at home working remotely over virtual desktop connections and for someone engaged in software development this is a setup that works well. Having to commute 90 minutes each way into London every day on the train is not something I’ll ever miss.

Team meetings are still an important part of the day though and that meant digging out and dusting off my old webcam, a Logitech something-or-other that works fine in every scenario except when I use it through my company’s Citrix-hosted virtual desktop. The video is fine but the audio frequency on the VDI is mismatched to the actual frequency on the physical device. I sound like Mickey Mouse on helium.

This is a well-known problem and the solution is to change the frequency on the VDI, which requires administrator level access. And that is never going to happen. I could try calling our support desk just to see how long it takes before they realise it’s not company-issued hardware and therefore the ticket has just been closed and is there anything else they can help me with today? No, not going to do that.

So I’ve been muddling through by using a rather useful Android USB microphone app called Wo-Mic. You install a PC server component, connect your phone by cable and voila your PC has a new USB microphone that’s actually your phone. It’s not a bad solution and the audio quality is very good but I’d really like a dedicated microphone that I can just plug in and place on the desk in front of my keyboard.

Any normal person would open Amazon and either buy the cheapest microphone available, or perhaps a Røde if they know decent audio when they hear it. Not me though. This sounds like a project if ever I heard one. A good chance to see if I can build a USB microphone and learn a thing or two along the way.

The design

I came up with two possible options for the design. One would be part-analogue and the other all-digital.

Both options are designed around a MEMS microphone for translating sound pressure levels into an electrical signal. Physically a MEMS microphone comes in a miniature metal ‘can’ that houses the required circuitry inside. The can serves both as physical protection for the sensitive receiver and as electrical protection from interference for the analogue circuitry. There is a small hole drilled in either the top or the bottom of the can to allow sound waves to enter.


This is a TDK ICS43432

If the port is on the bottom then you need to provide a hole of the same size in your PCB. This is the most tricky design for a hobbyist to work with because you absolutely must not get any gunk in that port which means being super-careful where you put flux on the pads that will be very, very close by.


The tiny circuit board on the bottom with those difficult pads

In option 1, the part-analogue design, I would connect a MEMS microphone up to some analogue signal conditioning and amplification circuitry before feeding it to an AD converter and then into an MCU for digital signal processing and output over a USB interface using the USB audio device class.

The tricky parts in this design are the signal conditioning and the ADC. MEMS microphones output a very low AC signal typically around ±1V. This would need to be amplified using an op-amp before sending to an analog-to-digital converter. I’ve modelled this design in LT-Spice and am convinced that I could do it but the devil would be in the implementation details such as noise problems from the mixed-signal circuit board, choice of op-amp and ADC.

Option 2 does away with those tricky analogue signal conditioning parts and uses a digital MEMS microphone. These devices have the analogue processing inside the metal can and their output is a digital I²S signal. From a noise-reduction and signal quality perspective keeping those sensitive analogue parts inside the shielding of the can is the best decision.

I decided to go with Option 2, the all-digital (to me) design.

Selecting a MEMS microphone

ST’s range of digital microphones provide a PDM output which needs some fairly involved but perfectly do-able software decoding in the MCU to get the PCM samples that I need. Best though are the microphones from TDK and Knowles that provide an I²S output. I²S is supported in hardware by many of the STM32 devices (it’s just a specific application of SPI) so the microphone can be hooked up directly to the MCU and we’ll be provided with a constant stream of PCM samples that we can work with directly.

I’ve had mixed sucess in other projects where I’ve used these microphones. The first build I did was a total write-off because like a complete idiot I washed the board after building it, immediately destroying the microphone by getting water in the port. Undeterred I built another one and it did work but, being an analogue microphone the audio quality wasn’t great because my first attempt at filtering and amplification didn’t hit the mark.

The next board I built was a different design that used a Knowles I²S microphone and I had big problems getting it to reflow to the board. I think the metal can caused problems with heat transfer to the pads underneath. And that’s the crux of the problem – the pads are completely underneath the microphone so it’s impossible to check for bad joints. I decided that going forward I’d simply buy a breakout board with the microphone already on it and design a carrier board around it.

This is the one I decided to use. It’s the INMP441 by TDK, Invensense or whatever they’re calling themselves this week. It’s actually gone NRND now but that doesn’t matter because I’m not going into mass production and I can get these breakout boards on ebay for less than £5.

MCU selection

It’s an STM32 and that’ll come as no surprise to anyone that follows this blog because they’re just so versatile. I need one that has I²S and USB peripherals and will be capable of translating the I²S format into the USB format in real time. Since this is a one-off build overkill is not an issue so I’ve selected the STM32F446RCT7 in the LQFP64 package.

This has 256kB of flash, 128kB of SRAM and can run at up to 180MHz. A lot of those resources are going to go unused but for the sake of a few pounds I’d rather have resources to spare than find I needed more to complete the project.

Detailed design

Here’s a more detailed view of the proposed design.

The STM32 sits at the center of the design and acts as an I²S master, providing the I²S clock and word select (WS) signals and receiving serial data as the response. Here’s a view of the protocol from the INMP441 datasheet.


The protocol in the middle is the one I’ll be using

The STM32 will provide a 48kHz WS signal and since there are 64 clocks per period then the clock will be 3.072MHz. Providing this clock accurately with zero error will require a dedicated external oscillator that you will see in the schematic. The data from the INMP441 is provided as 24-bit signed PCM samples, MSB first and left-justified into 32-bit words. The data bits are offset by one clock from the change in the WS signal and are available to read on the rising edge of the clock. This is known as the ‘Philips’ standard. Presumably the one clock offset was to allow early hardware implementations to use a clock cycle to reset their registers and prepare for a new sample.

The I²S peripheral will be connected to the DMA peripheral so it can operate in ‘hands free’ mode. The firmware will receive interrupts when buffers of data are ready to be processed and sent.

A buffer of data from the microphone will be presented as 64-bit samples with 24-bits of data in the left channel and zero in the right channel. ST provide a reference implementation of the USB audio device class that operates on 16-bit samples and I’ve decided not to try to change that, at least not in my first release. Therefore the first task is to downsample the sparse array of 64-bit samples into a new buffer of 16-bit PCM samples.

The next task in the pipeline is to apply any ‘graphic equalizer’ filtering that might be required. For example I might need to suppress high or low frequency noise to clean the signal up. If I’m really lucky the signal will be perfect out-of-the-box but somehow I doubt it.

Secondly I’ll need to adjust the signal volume (amplitude) according to my preference. The USB audio device class provides for a volume control and PC operating systems expose that in the form of a microphone volume slider in their ‘settings’ control panels.

Finally the fully transformed buffer of PCM signals can be sent to the USB firmware for transmission to the PC.

All of this has to be designed so that the number of samples that we gather in one DMA buffer is sufficiently large that we have enough time to do all the processing before the next buffer is available but cannot exceed the maximum size permitted to be sent in a single USB packet. This is why I’ve selected a 180MHz CPU with DSP instructions — finding that I’m CPU-bound would be a show-stopper for the project.

Schematic

Here’s the full schematic for this project.




Click for a PDF

The schematic is quite modular in design so let’s have a walk-through of each section.

The power supply and USB connection

5V is delivered to the board over the USB cable, filtered through the usual LC network that I use and connected to a Texas Instruments LP5907 3.3v ultra low noise regulator. The USB data signals and the 5V input are passed through an ST Micro USBLC6 ESD protection IC.

The INMP441 microphone

The footprint for the INMP441 is just two rows of 3 female pin headers spaced at 300mil. L/R is pulled down to GND with R5 which should cause the INMP441 to output data in the left channel. Rather than leave this hardwired I also decided to connect it up to the MCU just in case I needed to assert manual control over that line.

The INMP441 specification requires a 100k pull-down on the SD line and when I wrote this schematic my INMP441 was on the slow-boat from China so I decided to include the footprint for R2 and if it turned out that the board included it then I’d leave mine off the final build.

The STM32F446RC

The smallest available LQFP package has 64 pins so lots of them are going to be unused. On the left side we’ve got the I²S signals connected to the I2S3 peripheral. There’s also a simple GPIO input for a physical mute button on board. When muting is enabled the device will ignore DMA interrupts. USB I/O and the necessary SWD programming ports complete the left side of the picture.

Two LED outputs are provided. The blue link LED will light when the USB connection is active and running. If there’s a software crash in the form of a hard-fault then I’ll rapidly flash this LED and to that end I’m providing a reset button that can be used to get me out of this situation without having to unplug the USB cable. The red live LED will light up when data is actively being sent over the audio connection. When software or hardware muting is enabled then this light will go out.

Optimum USB audio quality requires accurate clocks. I2S_CK on PC9 is one such clock. It’s connected to an external 12.288MHz Microchip oscillator. Internally the STM32 can divide this by 256 to get exactly 48kHz and by 4 to get exactly 3.072MHz.

The STM32’s core clock will be derived from an external 8MHz crystal that also guarantees an accurate 48MHz clock for the USB peripheral.

Bill of materials

Here’s the full bill of materials for this project.

IdentifiersValueQuantityDescriptionFootprint
C110n1Ceramic capacitors0603
C2, C7, C8, C9, C10, C11, C15, C16, C18100n9Ceramic capacitors0603
C3, C6, C124.7µ3Ceramic capacitors0603
C4, C52Ceramic capacitors0603
C13, C1422p2Ceramic capacitors0603
C1722µ1electrolytic capacitor2.5mm lead pitch
D1Live LED1Red LED2012
D2Link LED1Blue LED2012
FB1BLM18PG221SN1D1Ferrite bead0603
P1USB connector1USB mini-Bcustom
P2INMP44112x 3-pin headers100mil headers, 300mil spacing between headers
P3JST XHP-51Female SWD header5x 2.5mm
R1, R2, R5100k3Chip SMD resistor0603
R33301Chip SMD resistor0805
P41501Chip SMD resistor0805
SW1, SW22PCB buttoncustom
U1STM32F446RCT71MCULQFP64
U2LP59071LDO regulatorSOT23-5
U3USBLC6-2SC61USB ESD protectionSOT23-6
Y1Abracon ABM3B18MHz crystalcustom
Y2DSC6011CI2A-012.2880T1Microchip 12.288MHz oscillatorcustom

PCB layout

Before starting the layout I had a quick look at the current JLCPCB prices and could scarcely believe my eyes when I saw that controlled impedance 4-layer boards are available up to 100x100mm for just $8 a pack of five. It wasn’t that long ago when 4 layer boards would run into hundreds of dollars!

Are 4-layer boards going to be the new normal? They are for me that’s for sure. The benefits of being able to just drop a via when you need power or ground are hard to ignore. The only minor annoyance with JLPCB’s implementation is that they won’t accept a negative gerber for an internal plane. You have to do everything with polygons and fills.

Here’s the PCB layout.


I shelved the polgon pours, including the two internal pours for these screenshots hence the rats-nest of apparently unconnected nets.

The first internal layer is GND and the second is VDD. The layout of the header pins for the INMP441 is designed so that the hole in its board that allows sound to pass through into the microphone can is facing upwards. I do have some concerns about dust getting into that hole over time so I may have to consider a housing for the microphone with some foam over that area.

The 5-pin SWD connector is a female JST XHP-5. These box connectors have a 2.5mm pitch and mate with a male connector fitted to a custom cable that I’ve made up. I now use these connectors for SWD connections when the board is small and won’t take the much bigger 10×2 100mil headers.

The 3D view is best for checking whether any of the silkscreen labels overlap parts that they should not and making minor changes to their position. Four M3 screw holes round off a very simple physical design measuring around 46mm square.

Building the board

I uploaded my design to the JLPCB website, selected a black solder mask and used the 3 weeks it took to arrive to source the parts from Mouser.


JLPCB were the first to offer a matte-black soldermask at a reasonable price and I do have one of their early boards and the finish was very similar to a black chalkboard. Apparently that finish suffered from poor adhesion and so they’ve now changed it slightly and whilst it’s still matte there’s a very slight shine to it and it’s more black than grey. It does look very nice and not at all like the horrible glossy uneven black of old.

My process for building a board is quite slow but I find it reliable. I first tin the pads with solder, then I reapply flux to the pads, then I place the surface mount components on the tinned pads and then I reflow the board in my halogen reflow oven. When the reflow is complete I touch up any problems manually and then solder in the through-hole components by hand. Finally I wash the board using hot soapy water and a toothbrush, rinse it off with cold water and leave it out to dry for at least a day.

Looking good! The reflow was completely successful with no touch-up required. Now I can get on with the firmware development.

Firmware

I wrote the firmware in Ubuntu Linux using the STM32Cube IDE. This is basically Eclipse with plugins developed by ST Micro as well as some other open-source plugins. To kick-start the project I used the Cube GUI to configure the peripherals and clocks and then write out a template project. Here’s the peripherals view:

It’s very helpful to be able to use the graphical clock tree configurator to set everything up and know in advance that it’s going to be perfect.

Once Cube has generated the template project then I take it from there. I edit the generated source code to remove the huge ugly (sorry ST) comments and reformat the source to make it more readable.

Debugging

The first time I hooked everything up I was initially pleased to find that the MCU was responding, firmware was being flashed and I was getting regular interrupts from the DMA controller handling the I²S peripheral. On the USB side my microphone was detected by Ubuntu and I was able to capture samples using the free Audacity software.

However, it sounded dreadful. My voice was audible but it was harsh and way too loud. If I got anywhere near the microphone then it would clip badly and harshly. Something was clearly wrong.

The difficulty with debugging audio is that you’re presented with a continuous stream of what you have to assume is valid PCM samples but what does a correct sample look like? You can’t just look at a buffer of data in the debugger and be able to tell good from bad. I’ve been known to hold up the microphone to my computer speakers whilst playing an online sine-wave generator to see if the captured data would exhibit a constant pattern!

The first thing I did was verify what could easily be probed. The voltages were fine. My oscilloscope showed exactly 48kHz on the WS line and exactly 3.072MHz on the CK line so no problems there.

To rule out the microphone I ordered another one online from a different seller and waited a few days. The new microphone showed exactly the same issue so it was apparent that the problem was elsewhere.

For the next step I decided that I needed to know exactly how the DMA peripheral was delivering the I²S data into memory. I could see from the data in-memory that the first 32-bit word had the data and the second was always zero but was the 24-bit sample in the 32-bit double-word being byte-swapped or swapped around 16-bit words? This was a nagging concern that had to be investigated because the DMA peripheral is programmed for 32-bit transfers but the I²S peripheral is inherently a 16-bit device.

I got out an old STM32F4 discovery board and wrote a simple firmware program to act as an I²S slave that would output fixed 24-bit samples. I removed the INMP441 from my board and hooked up the discovery board, splicing in my logic analyser into the middle so I could see what was going on.

This was very enlightening. Not only could I see exactly how a 24-bit value in memory is placed on to the wire by the I²S peripheral in the discovery board but I could also see how that would end up in the receiving board’s memory.

The issue, in the end, was that I was losing the sign bit on the PCM sample during my processing. To downsample from 24 to 16 bits I simply take the top 16-bits, discarding the lower 8. With the sign bit preserved correctly the audio started working.

Audio processing

ST provide a suite of audio effects expansion software called X-CUBE-AUDIO. This is implemented as a closed-source but freely available package that integrates easily into firmware using consistent APIs designed to be used as part of an audio-processing pipeline.

The first package I was interested in was the Graphic Equalizer (GREQ) package documented in UM1798. The audio that I was sampling was clear and noise-free but because of the location where I sit it tended to sound quite boomy and bass-heavy. If I could attenuate those frequencies then my speaking voice would sound more crisp and natural with less reverberation.


You can select the center frequencies for the equalizer configuration from 5, 8 or 10 preset bands with a maximum adjustment of ±12dB. I selected 10 bands and configured it to attenuate 62, 115 and 215Hz by -6dB and to amplify the other frequencies by +6dB. The reason for the amplification of the higher frequencies is to compensate for an overall attenuation performed by the filter — see UM1798 for details.

This is the resource usage required by the GREQ filter.

To use the filters I simply copied the two header files and the binary library into my project, rebuilt and ran. Surprisingly and very happily it just worked first time. The audio was notably more clear with much less unwanted boomy reverberation.

I will have to leave these settings hardcoded into the firmware because although the USB audio device class does provide for graphic equalizer control I have not seen it implemented in PC operating system software even though I declare in my audio device descriptor that I support it and I can see it there in the UsbTreeView PC software.

The next part would be volume control. Until now I’d used the normalisation filter in Audacity to bring up the input signal to a loud enough level to listen to critically. This stage in the pipeline is achieved by ST’s Smart Volume Control (SVC) filter documented in UM1642.

It is possible to do dumb amplification by simply multiplying the PCM signals by a constant value and if the levels are low enough you might get away with that without clipping but you’ll also amplify any noise in the signal because you’ll treat low levels where there is no useful sound the same as the higher levels. ST’s filter takes into account the dynamic range of the input signal to give more consideration to the peaks without touching the troughs.

The configuration for this filter is a simple selection of the gain from -80 to +36dB in 0.5dB steps so the actual limits for the parameter are -160 to +72. This is the resource usage required by the SVC filter.

Once again this just worked and I was able to hook up the volume control to the USB control input so I could use the volume slider in the Ubuntu settings app to set it on-the-fly.

I have found that I need to apply the full +36dB amplification to get acceptable volume from the microphone. I used the Skype echo test call to check that I’m sounding good and, Skype’s obvious compression of the voice channel aside, it’s all good on the audibility front.

Here’s the important interrupt handler that processes the incoming samples.

/**
 * 1. Transform the I2S data into 16 bit PCM samples in a holding buffer
 * 2. Use the ST GREQ library to apply a graphic equaliser filter
 * 3. Use the ST SVC library to adjust the gain (volume)
 * 4. Transmit over USB to the host
 *
 * We've got 10ms to complete this method before the next DMA transfer will be ready.
 */

inline void Audio::sendData(volatile int32_t *data_in, int16_t *data_out) {

  // only do anything at all if we're not muted and we're connected

  if (!_muteButton.isMuted() && _running) {

    // transform the I2S samples from the 64 bit L/R (32 bits per side) of which we
    // only have data in the L side. Take the most significant 16 bits, being careful
    // to respect the sign bit.

    int16_t *dest = _processBuffer;

    for (uint16_t i = 0; i < MIC_SAMPLES_PER_PACKET / 2; i++) {
      *dest++ = data_in[0];     // left channel has data
      *dest++ = data_in[0];     // right channel is duplicated from the left
      data_in += 2;
    }

    // apply the graphic equaliser filters using the ST GREQ library then
    // adjust the gain (volume) using the ST SVC library

    _graphicEqualiser.process(_processBuffer, MIC_SAMPLES_PER_PACKET / 2);
    _volumeControl.process(_processBuffer, MIC_SAMPLES_PER_PACKET / 2);

    // we only want the left channel from the processed buffer

    int16_t *src = _processBuffer;
    dest = data_out;

    for (uint16_t i = 0; i < MIC_SAMPLES_PER_PACKET / 2; i++) {
      *dest++ = *src;
      src += 2;
    }

    // send the adjusted data to the host

    if (USBD_AUDIO_Data_Transfer(&hUsbDeviceFS, data_out, MIC_SAMPLES_PER_PACKET / 2) != USBD_OK) {
      Error_Handler();
    }
  }
}

/**
 * Override the I2S DMA half-complete HAL callback to process the first MIC_MS_PER_PACKET/2 milliseconds
 * of the data while the DMA device continues to run onward to fill the second half of the buffer.
 */

inline void Audio::I2S_halfComplete() {
  sendData(_sampleBuffer, _sendBuffer);
}

/**
 * Override the I2S DMA complete HAL callback to process the second MIC_MS_PER_PACKET/2 milliseconds
 * of the data while the DMA in circular mode wraps back to the start of the buffer
 */

inline void Audio::I2S_complete() {
  sendData(&_sampleBuffer[MIC_SAMPLES_PER_PACKET], &_sendBuffer[MIC_SAMPLES_PER_PACKET / 2]);
}

The DMA peripheral is configured to transfer 20ms of data into our buffer and to provide 'half-complete' and 'complete' interrupts. Therefore we have 10ms to decode, process and send 10ms of data before the next interrupt happens.

The first stage decodes the 64-bit mono samples from _sampleBuffer into 16-bit interleaved stereo signals in _processBuffer. Stereo is required because the GREQ filter will not operate on mono. To simulate stereo I simply duplicate the left channel into the right.

The second stage calls the GREQ filter to process the data in-place. Nice that these filters can work on data in-place.

The third stage calls the SVC filter to also process the data in-place.

The final stage takes the processed left channel from _processBuffer, copies it into _sendBuffer and calls USBD_AUDIO_Data_Transfer to transmit it. USBD_AUDIO_Data_Transfer has some constraints. It cannot be called more frequently than once per millisecond — check. You must pass it an amount of data that matches the calling frequency — I'm calling it once every 10ms with 480 mono samples which is exactly correct for a 48kHz stream. There may also be an upper cap of 1000 bytes on the packet to match a USB buffer size but that's not documented in the ST source code.

Performance

To measure the performance of the audio transformation and processing pipeline I inserted some debug code to toggle a GPIO pin at the start of the sendData method and then again just after the call to USBD_AUDIO_Data_Transfer. The actual data transfer performed by USBD_AUDIO_Data_Transfer is interrupt-driven so that part is not included in the performance figures. I used my oscilloscope to probe the oscillating GPIO pin and measured the time between the rising and falling edges.

Recall that I have 10ms to do all the work in the interrupt handler. In debug mode processing takes 1.7ms. In release mode it takes 1.5ms. I'm pleasantly surprised by this performance and it does indicate that the opaque audio processing blocks provided by ST Micro perform very well.

Watch the video

I've made short video that talks about this project and shows the microphone in operation with some sound samples so you can hear it.

It looks better when viewed directly from the YouTube website. Click here for that.

Free gerber files

If you'd like to build this board yourself then you can download the Gerber files from here. These can be uploaded to the JLPCB website.

Free firmware

It's all available on Github. Click here to go to the repository.

Fixing the USB microphone mute button click

$
0
0

In my previous article I documented the design and build process of my USB microphone based around an STM32F446 MCU. If you haven’t already read it then it’s probably worth catching up now before reading the rest of this article so that you have the necessary context.

The problem

I’ve been using the microphone for a while now and never really noticed that there was a noise issue with the hardware mute button until I recorded a sound file using Audacity that featured me coming in and out of mute. The noise issue is caused by my poor choice of hardware button:

These buttons are cheap PCB mounted momentary press-release buttons that have an audible click both on the press and the subsequent release. Unfortunately, because the button is located close to the INMP441 sensor the click is very audible.

Click here to listen to the problem. I come out of mute at the start and go back in at the end.

Given that I’m using this microphone daily for virtual meetings I can only assume that either the Skype audio is so bad that you can’t differentiate these clicks from the usual audio corruption that Skype randomly applies or my colleagues are too polite to tell me that I’m clicking at them.

The fix

I didn’t want to desolder the button from the board and bodge in a momentary button that doesn’t click because that would ruin the nice neat appearance of the board. Instead I decided to see what I could do in software.

General approach

Instead of always reacting to a button down event I would need to get smarter and react on the upward or downward button transition depending on whether I was going into, or coming out of mute. This needed a redesign of my generic Button class to inform me when either a new up or down state was reached, with a different reaction delay for up and down. Here’s the modified class:

/*
 * This file is part of the firmware for the Andy's Workshop USB Microphone.
 * Copyright 2021 Andy Brown. See https://andybrown.me.uk for project details.
 * This project is open source subject to the license published on https://andybrown.me.uk.
 */

#pragma once

/**
 * A debounced button class
 */

class Button: public GpioPin {

  public:
    enum CurrentState {
      None,
      Up,
      Down
    };

  private:

    static const uint32_t DEBOUNCE_UP_DELAY_MILLIS = 100;
    static const uint32_t DEBOUNCE_DOWN_DELAY_MILLIS = 1;

    enum InternalState {
      Idle,                         // nothing happening
      DebounceUpDelay,              // delaying...
      DebounceDownDelay
    };

    bool _pressedIsHigh;            // The button is electrically HIGH when pressed?
    InternalState _internalState;   // Internal state of the class

    bool _lastButtonReading;        // the last state we sampled
    uint32_t _transitionTime;       // the time of the last transition

  public:
    Button(const GpioPin &pin, bool pressedIsHigh);
    CurrentState getAndClearCurrentState();   // retrieve current state and reset to idle
};

inline Button::Button(const GpioPin &pin, bool pressedIsHigh) :
    GpioPin(pin) {

  _transitionTime = 0;
  _lastButtonReading = false;
  _pressedIsHigh = pressedIsHigh;
  _internalState = Idle;
}

/**
 * Get and reset the current state. This should be called in the main application loop.
 * @return The current state. If the current state is one of the up/down pressed states
 * then that state is returned and then internally reset to none so the application only
 * gets one 'notification' that the button is pressed/released.
 */

inline Button::CurrentState Button::getAndClearCurrentState() {

  // read the pin and flip it if this switch reads high when open

  bool buttonReading = getState();
  if (!_pressedIsHigh) {
    buttonReading ^= true;
  }

  const uint32_t now = HAL_GetTick();

  if (_lastButtonReading == buttonReading) {

    // no change in the button reading, we could be exiting the debounce delay

    switch (_internalState) {

    case DebounceUpDelay:
      if (now - _transitionTime > DEBOUNCE_UP_DELAY_MILLIS) {
        _internalState = Idle;
        return Up;
      }
      break;

    case DebounceDownDelay:
      if (now - _transitionTime > DEBOUNCE_DOWN_DELAY_MILLIS) {
        _internalState = Idle;
        return Down;
      }
      break;

    case Idle:
      break;
    }

    return None;

  } else {

    // button reading has changed, this always causes the state to enter the debounce delay

    _transitionTime = now;
    _lastButtonReading = buttonReading;
    _internalState = buttonReading ? DebounceDownDelay : DebounceUpDelay;

    return None;
  }
}

The user of this class calls getAndClearCurrentState() in the main loop and it will return Up or Down exactly once when there’s a transition and None otherwise.

Going into mute

When going into mute I want muting to happen immediately when the button is pressed down, hopefully before it emits a click. When the button comes back up it won’t matter because we’ll be in the muted state. To get that immediate response I set the debounce delay for a button down transition to 1ms in the Button class.

Coming out of mute

I want the transition out of mute to happen when the button comes up, and I want it to be delayed sufficiently that the click caused by releasing the button is skipped. To achieve that I set the debounce delay for a button up transition to 100ms in the Button class.

The above logic is encapsulated in the MuteButton subclass that distills everything down into a single isMuted method to return the current state.

/*
 * This file is part of the firmware for the Andy's Workshop USB Microphone.
 * Copyright 2021 Andy Brown. See https://andybrown.me.uk for project details.
 * This project is open source subject to the license published on https://andybrown.me.uk.
 */

#pragma once

class MuteButton: public Button {

  private:
    volatile bool _muted;
    bool _ignoreNextUp;

  public:
    MuteButton();

    void run();
    bool isMuted() const;
};

inline MuteButton::MuteButton() :
    Button(GpioPin(MUTE_GPIO_Port, MUTE_Pin), false) {

  _muted = false;
  _ignoreNextUp = false;
}

inline void MuteButton::run() {

  Button::CurrentState state = getAndClearCurrentState();

  // check for idle

  if (state == Button::CurrentState::None) {
    return;
  }

  if (state == Down) {

    if (!_muted) {
      _muted = true;
      _ignoreNextUp = true;   // the lifting of the button shouldn't exit mute
    }
  }
  else {

    if (_muted) {

      if (_ignoreNextUp) {

        // this is the lifting of the button that went into mute

        _ignoreNextUp = false;
      }
      else {
        _muted = false;
      }
    }
  }
}

inline bool MuteButton::isMuted() const {
  return _muted;
}

Testing and more fixes

With the above fixes I ran some tests by recording with Audacity and repeatedly pressing the mute button. For the most part it worked as I hoped but sometimes, about 1 in 5, there was still a 'pop' spike at either transition but now it sounded more like a pop due to a problem with the audio encoding rather than the 'click' of the button.

When muted my code zeros out the audio buffer before sending to the host and so, based on the hunch that this could cause a 'pop' sound at the transition into and/or out of mute I decided to change to enabling the soft 'mute' function of ST's Smart Volume Control (SVC) library that I was already using to control volume. The documentation in UM1642 has this to say about the mute function:

The SVC "mute" dynamic parameter mutes the output when set to 1 or has no influence on input signal when set to 0. When enabled, it allows mute the signal smoothly over a frame, avoiding audible artifacts.

This approach appeared to totally solve the problem when coming out of mute, the pop had totally gone no matter how many times I pressed and released the button. However, going into mute (where a fast reaction is required) it only improved slightly on the previous results. I suspect that the algorithm that smooths out the transitions is too slow to catch the pop sound.

To fix this last issue I went back to the method of zeroing out data frames. I found by experimentation that by zeroing out the first 500ms of data when transitioning into mute it solved the problem for at least 9/10 cases and I'm happy with that. The core sendData audio interrupt handler now looks like this.

inline void Audio::sendData(volatile int32_t *data_in, int16_t *data_out) {

  // only do anything at all if we're connected

  if (_running) {

    // ensure that the mute state in the smart volume control library matches the mute
    // state of the hardware button. we do this here to ensure that we only call SVC
    // methods from inside an IRQ context.

    if (_muteButton.isMuted()) {
      if (!_volumeControl.isMuted()) {
        _volumeControl.setMute(true);

        // the next 50 frames (500ms) will be zero'd - this seems to do a better job of catching the
        // mute button 'pop' than the SVC filter mute when going into a mute

        _zeroCounter = 50;
      }
    }
    else {
      if (_volumeControl.isMuted()) {

        // coming out of a mute is handled well by the SVC filter

        _volumeControl.setMute(false);
      }
    }

    if (_zeroCounter) {
      memset(data_out, 0, (MIC_SAMPLES_PER_PACKET * sizeof(uint16_t)) / 2);
      _zeroCounter--;
    }
    else {

      // transform the I2S samples from the 64 bit L/R (32 bits per side) of which we
      // only have data in the L side. Take the most significant 16 bits, being careful
      // to respect the sign bit.

      int16_t *dest = _processBuffer;

      for (uint16_t i = 0; i < MIC_SAMPLES_PER_PACKET / 2; i++) {

        // dither the LSB with a random bit

        int16_t sample = (data_in[0] & 0xfffffffe) | (rand() & 1);

        *dest++ = sample;     // left channel has data
        *dest++ = sample;     // right channel is duplicated from the left
        data_in += 2;
      }

      // apply the graphic equaliser filters using the ST GREQ library then
      // adjust the gain (volume) using the ST SVC library

      _graphicEqualiser.process(_processBuffer, MIC_SAMPLES_PER_PACKET / 2);
      _volumeControl.process(_processBuffer, MIC_SAMPLES_PER_PACKET / 2);

      // we only want the left channel from the processed buffer

      int16_t *src = _processBuffer;
      dest = data_out;

      for (uint16_t i = 0; i < MIC_SAMPLES_PER_PACKET / 2; i++) {
        *dest++ = *src;
        src += 2;
      }
    }

    // send the adjusted data to the host

    if (USBD_AUDIO_Data_Transfer(&hUsbDeviceFS, data_out, MIC_SAMPLES_PER_PACKET / 2) != USBD_OK) {
      Error_Handler();
    }
  }
}

Click here to listen to the audio after these fixes have been applied. I come out of mute at the start and go back in at the end.

Conclusion

Well obviously I should have thought ahead that a microphone project shouldn't really have noisy components situated right next to the sensor but at least I've been able to almost totally fix the problem with software alone so I'll have confidence using the mute feature in virtual meetings now.

The code fix has just been merged to master in Github so if you pull the latest changes you'll get the fixes.

Viewing all 25 articles
Browse latest View live