Microcontroller projects
A project is not just a longer program: it is a way of working. What separates a prototype that runs on the bench from a device that runs for a whole year in a plant is not the code, it is the method.
01From the idea to the device
| Stage | What is defined | What is delivered |
|---|---|---|
| 1 | Requirements: what it must do, with what accuracy, under what conditions, for whom. | List of verifiable requirements. |
| 2 | Architecture: blocks, the signals between them, choice of the microcontroller and the sensors. | Block diagram and pin budget. |
| 3 | Prototype in parts: each block is tested on its own before it is integrated. | Modules verified one by one. |
| 4 | Integration: the blocks are put together and the timing and noise problems appear. | Complete working prototype. |
| 5 | Board: schematic, printed circuit board, assembly. | Device in its final form. |
| 6 | Testing: functional, limit, endurance and fault-recovery tests. | Test protocol with results. |
| 7 | Documentation and delivery. | Schematics, code, manual and backup. |
The schematic and the board are developed with what was covered in schematics and printed circuit boards.
“It should measure temperature well” is not a requirement: it cannot be checked. “Measure between −10 and +80 °C with an error under ±0.5 °C, updating every second, powered from 12 V and operating at ambient temperatures from 0 to 50 °C” is. The difference looks like a formality and it is not: the verifiable requirement is what gets tested later, and what avoids the argument over whether the device is finished.
02The state machine
typedef enum { REPOSO, LLENANDO, ESTABILIZANDO, DESCARGANDO, FALLA } estado_t; estado_t estado = REPOSO; void maquina(void) { switch (estado) { case REPOSO: if (hay_pedido() && recipiente_ok()) { abrir_valvula(); estado = LLENANDO; } break; case LLENANDO: if (peso() >= objetivo) { cerrar_valvula(); t0 = ahora(); estado = ESTABILIZANDO; } else if (ahora() - t0 > T_MAX) { cerrar_valvula(); estado = FALLA; } break; case ESTABILIZANDO: if (ahora() - t0 > 500) { registrar(peso()); estado = DESCARGANDO; } break; case DESCARGANDO: if (peso() < VACIO) { estado = REPOSO; } break; case FALLA: todo_seguro(); if (rearme()) { estado = REPOSO; } break; } }
Because the function does not block: it enters, evaluates, returns. It can be called a thousand times per second from the main loop, while the same loop serves the display, the communication and the pushbuttons. With waits inside the sequence, everything else stops.
In addition, each state has its own maximum time. A system that can wait forever for an event that never arrives is a system that will hang at some point.
03A main loop that never blocks
int main(void) { inicializar(); wdt_habilitar(WDT_2S); /* 2-second watchdog timer */ while (1) { uint32_t t = ms_actual(); if (t - t_control >= 10) { t_control = t; leer_sensores(); maquina(); } if (t - t_pantalla >= 300) { t_pantalla = t; refrescar_pantalla(); } if (t - t_registro >= 1000){ t_registro = t; guardar_dato(); } atender_serie(); /* non-blocking: processes whatever is there */ wdt_refrescar(); /* if the loop gets stuck, the device resets */ } }
Each task runs at its own pace and none of them waits. It is the closest thing to a real-time operating system that you can have without using one, and for the vast majority of devices it is more than enough.
The condition is that no function takes too long: if
guardar_dato() takes 300 ms writing to a memory, the 10 ms control task is missed
thirty times. Long functions are split into steps, or moved into the state machine.
04Still working next month
- Watchdog timer enabled and refreshed in a single place. Never inside an interrupt: it would lose its purpose.
- Safe default values if the stored configuration is corrupt, detected with a checksum.
- Validated ranges on every input: a disconnected sensor usually reads zero or full scale, and that cannot be taken as good data.
- An explicit fault state, which leaves everything in a safe condition and requires a deliberate reset.
- Event log: how many times it reset, when and why. It is what makes remote diagnosis possible.
- 100 nF decoupling capacitors on every supply pin, right next to the chip.
- Inputs with filtering and protection; outputs with a flyback (freewheeling) diode if they drive coils.
- Reset with its capacitor and, if needed, a supply supervisor.
- Unused pins defined, not left floating.
- Power supply with margin: a regulator at its limit is a failure waiting for summer.
- Power off and on at the worst moment, many times, even during a write to memory.
- Disconnect each sensor and each actuator while the device is running.
- Leave it running for several days in a row: counter overflows and memory leaks only show up then.
- Out-of-range inputs, buttons pressed at the same time, malformed serial commands.
- High and low temperature, and supply voltage variation over its whole range.
05Versioning and documentation
| Item | What it must contain |
|---|---|
| Version control | The code in a repository, with small changes and messages that say why. Every delivered version, tagged. |
| Version in the device | The version number visible on the display or through a serial command. Without it, you cannot tell what is running on each unit. |
| Schematic and board | Source files and PDF, with the revision marked on the board itself. |
| User manual | How to operate it, what each indication means and what to do about each error message. |
| Technical manual | How to calibrate it, how to update the program, what is checked during maintenance and what spare parts it uses. |
| Backup | Code, schematics, manufacturing files and component datasheets, all together and stored away from the working machine. |
That another person can pick up the project, understand it, compile it and modify it without asking anything. If that is not possible, the project is incomplete even though the device works. And it is worth measuring it for real: hand the folder to a classmate and see how far they get on their own.
06In the lab
Take a project idea and write its requirements in a verifiable way, with numbers and conditions. Swap with another group: if someone can interpret a requirement in two different ways, it is badly written and must be corrected.
Write a simple controller with waits and verify that the display and the buttons stop responding while it runs. Rewrite it as a non-blocking state machine and check that everything responds at the same time.
Enable the watchdog timer and cause a deliberate lock-up inside a function. Verify that the device resets by itself and logs the reset. Then move the watchdog refresh to a wrong place and check that it no longer protects.
Take a project of your own through the seven stages, delivering each document. The whole path is assessed, not just whether the device powers up: requirements, tests and documentation weigh as much as the operation itself.
07Common mistakes
| Mistake | Consequence |
|---|---|
| Starting to program without requirements | The project never ends: something that nobody defined is always missing. |
| Integrating everything at once | When it fails, you do not know which of the ten blocks is at fault. Each block is tested alone first. |
| Blocking waits in the main loop | The interface stops responding and the control loop loses its timing. |
| States without a maximum time | The device can wait forever for an event that never arrives. |
| Refreshing the watchdog timer in an interrupt | The main program can be hung and the watchdog never acts. |
| Not validating input ranges | A disconnected sensor reads zero and the controller acts on false data. |
| No version number in the device | It is impossible to know which program each unit in the field has. |
| Testing only on the bench, for a few minutes | Problems of endurance, temperature and power loss appear later, at the customer’s site. |
08Self-assessment
What makes a requirement verifiable?
That it has numbers and conditions: range, permissible error, response time, operating conditions. It is what gets tested later and what defines whether the device is finished.
Why is each block tested separately before integration?
Because if everything is integrated at once and it fails, there is no way to know which block is the problem. Testing in parts shortens the diagnosis enormously.
What advantage does a state machine have over a sequence with waits?
It does not block: it can be called continuously from the main loop while the display, communication and buttons are served. With waits, everything else stops.
Why must each state have a maximum time?
So that the system does not wait indefinitely for an event that is not going to arrive. When the time runs out, it moves to a safe fault state.
What is cooperative scheduling?
A main loop where each task runs at its own pace by comparing times, with no waits. It requires that no function takes too long.
Where is the watchdog timer refreshed, and why?
In a single place in the main loop. If it were refreshed from an interrupt, the loop could be hung and the watchdog would not detect it.
Why must the range of the inputs be validated?
Because a disconnected or faulty sensor usually reads zero or full scale, and the controller would take that value as good and act on false data.
Name three tests that cannot be left out.
Power loss at critical moments, disconnecting sensors and actuators while the device is running, and continuous operation for several days.
Why must the device show its version number?
Because in the field there are units with different versions of the program, and without that information it is impossible to know which one each has or to reproduce a problem.
What is the acid test of the documentation?
That another person can pick up the project, understand it, compile it and modify it without asking anything.